Repository: appvia/kev
Pull request #482 (2021-04-23 10:56:54)

kev-458 remove label validation


Patch:
diff --git a/pkg/kev/config/extensions.go b/pkg/kev/config/extensions.go
index 197f545..3c79348 100644
--- a/pkg/kev/config/extensions.go
+++ b/pkg/kev/config/extensions.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/appvia/kev/pkg/kev/log"
 	composego "github.com/compose-spec/compose-go/types"
 	"github.com/go-playground/validator/v10"
 	"github.com/imdario/mergo"
@@ -22,7 +23,7 @@ type ExtensionRoot struct {
 
 // K8SConfiguration represents the root of the k8s specific fields supported by kev.
 type K8SConfiguration struct {
-	Enabled  bool     `yaml:"enabled,omitempty"`
+	Disabled bool     `yaml:"disabled"`
 	Workload Workload `yaml:"workload" validate:"required,dive"`
 	Service  Service  `yaml:"service,omitempty"`
 }
@@ -71,12 +72,13 @@ func (k K8SConfiguration) Validate() error {
 // DefaultK8SConfig returns a K8SServiceConfig with all the defaults set into it.
 func DefaultK8SConfig() K8SConfiguration {
 	return K8SConfiguration{
-		Enabled: DefaultServiceEnabled,
+		Disabled: false,
 		Workload: Workload{
 			Type:           DefaultWorkload,
 			LivenessProbe:  DefaultLivenessProbe(),
 			ReadinessProbe: DefaultReadinessProbe(),
 			Replicas:       1,
+			RestartPolicy:  RestartPolicyAlways,
 		},
 		Service: Service{
 			Type: "None",
@@ -108,7 +110,6 @@ func RequireExtensions() K8SCfgOption {
 func K8SCfgFromCompose(svc *composego.ServiceConfig) (K8SConfiguration, error) {
 	var cfg K8SConfiguration
 
-	cfg.Enabled = true
 	cfg.Workload.Type = WorkloadTypeFromCompose(svc)
 	cfg.Workload.Replicas = WorkloadReplicasFromCompose(svc)
 	cfg.Workload.RestartPolicy = WorkloadRestartPolicyFromCompose(svc)
@@ -137,19 +138,27 @@ func K8SCfgFromCompose(svc *composego.ServiceConfig) (K8SConfiguration, error) {
 	return cfg, nil
 }
 
+// WorkloadRestartPolicyFromCompose infers a kev-valid restart policy from compose data.
 func WorkloadRestartPolicyFromCompose(svc *composego.ServiceConfig) string {
-	if svc.Deploy == nil || svc.Deploy.RestartPolicy == nil {
-		return ""
+	policy := RestartPolicyAlways
+
+	if svc.Restart != "" {
+		policy = svc.Restart
 	}
 
-	switch svc.Deploy.RestartPolicy.Condition {
-	case "on-failure":
-		return RestartPolicyOnFailure
-	case "none":
-		return RestartPolicyNever
-	default:
-		return RestartPolicyAlways
+	if svc.Deploy != nil && svc.Deploy.RestartPolicy != nil {
+		policy = svc.Deploy.RestartPolicy.Condition
 	}
+
+	if policy == "unless-stopped" {
+		log.WarnWithFields(log.Fields{
+			"restart-policy": policy,
+		}, "Restart policy 'unless-stopped' is not supported, converting it to 'always'")
+
+		policy = "always"
+	}
+
+	return policy
 }
 
 func WorkloadReplicasFromCompose(svc *composego.ServiceConfig) int {
@@ -189,7 +198,7 @@ func LivenessProbeFromCompose(svc *composego.ServiceConfig) LivenessProbe {
 	res.Type = ProbeTypeExec.String()
 
 	test := healthcheck.Test
-	if len(test) > 0 && strings.ToLower(test[0]) == "cmd" {
+	if len(test) > 0 && (strings.ToLower(test[0]) == "cmd" || strings.ToLower(test[0]) == "cmd-shell") {
 		test = test[1:]
 	}
 	res.Exec.Command = test
diff --git a/pkg/kev/config/labels.go b/pkg/kev/config/labels.go
index 30feb37..11ee1a4 100644
--- a/pkg/kev/config/labels.go
+++ b/pkg/kev/config/labels.go
@@ -19,8 +19,7 @@ package config
 const (
 	LabelPrefix = "kev."
 
-	// LabelComponentEnabled toggles the project service component
-	LabelComponentEnabled = LabelPrefix + "component.enabled"
+	// LabelComponentEnabled = LabelPrefix + "component.enabled"
 
 	// LabelWorkloadType defines type of workload controller
 	LabelWorkloadType = LabelPrefix + "workload.type"
diff --git a/pkg/kev/config/schema.go b/pkg/kev/config/schema.go
index 389377f..289e868 100644
--- a/pkg/kev/config/schema.go
+++ b/pkg/kev/config/schema.go
@@ -16,136 +16,6 @@
 
 package config
 
-// ServicesSchema defines validation rules for service labels
-var ServicesSchema = map[string]interface{}{
-	"$schema": "http://json-schema.org/draft/2019-09/schema#",
-	"$id":     "http://appvia.io/schemas/kev-service-labels-schema.json",
-	"type":    "object",
-	"properties": map[string]interface{}{
-		LabelComponentEnabled:                            map[string]interface{}{"type": "string", "enum": []string{"false", "true"}},
-		LabelWorkloadCPU:                                 map[string]interface{}{"type": "string"},
-		LabelWorkloadImagePullPolicy:                     map[string]interface{}{"type": "string", "pattern": `^(?i)(IfNotPresent|Always)$`},
-		LabelWorkloadImagePullSecret:                     map[string]interface{}{"type": "string"},
-		LabelWorkloadRestartPolicy:                       map[string]interface{}{"type": "string", "pattern": `^(?i)(Always|OnFailure|Never)$`},
-		LabelWorkloadLivenessProbeType:                   map[string]interface{}{"type": "string", "enum": []string{"none", "exec", "http", "tcp"}},
-		LabelWorkloadLivenessProbeCommand:                map[string]interface{}{"type": "string"},
-		LabelWorkloadLivenessProbeHTTPPort:               map[string]interface{}{"type": "string"},
-		LabelWorkloadLivenessProbeTCPPort:                map[string]interface{}{"type": "string"},
-		LabelWorkloadLivenessProbeHTTPPath:               map[string]interface{}{"type": "string"},
-		LabelWorkloadLivenessProbeInitialDelay:           map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadLivenessProbeInterval:               map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadLivenessProbeRetries:                map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadLivenessProbeTimeout:                map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadReadinessProbeType:                  map[string]interface{}{"type": "string", "enum": []string{"none", "exec", "http", "tcp"}},
-		LabelWorkloadReadinessProbeCommand:               map[string]interface{}{"type": "string"},
-		LabelWorkloadReadinessProbeHTTPPort:              map[string]interface{}{"type": "string"},
-		LabelWorkloadReadinessProbeTCPPort:               map[string]interface{}{"type": "string"},
-		LabelWorkloadReadinessProbeHTTPPath:              map[string]interface{}{"type": "string"},
-		LabelWorkloadReadinessProbeInitialDelay:          map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadReadinessProbeInterval:              map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadReadinessProbeRetries:               map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadReadinessProbeTimeout:               map[string]interface{}{"type": "string", "format": "duration"},
-		LabelWorkloadMaxCPU:                              map[string]interface{}{"type": "string"},
-		LabelWorkloadMaxMemory:                           map[string]interface{}{"type": "string", "pattern": `^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$`},
-		LabelWorkloadMemory:                              map[string]interface{}{"type": "string", "pattern": "^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"},
-		LabelWorkloadReplicas:                            map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadAutoscaleMaxReplicas:                map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadAutoscaleCPUUtilizationThreshold:    map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadAutoscaleMemoryUtilizationThreshold: map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadRollingUpdateMaxSurge:               map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadServiceAccountName:                  map[string]interface{}{"type": "string", "pattern": `^[a-zA-Z0-9._-]+$`},
-		LabelWorkloadType:                                map[string]interface{}{"type": "string", "pattern": `^(?i)(Pod|Deployment|StatefulSet|Daemonset|Job)$`},
-		LabelWorkloadSecurityContextRunAsUser:            map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadSecurityContextRunAsGroup:           map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelWorkloadSecurityContextFsGroup:              map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelServiceType:                                 map[string]interface{}{"type": "string", "pattern": `^(?i)(None|Headless|ClusterIP|Nodeport|LoadBalancer)$`},
-		LabelServiceNodePortPort:                         map[string]interface{}{"type": "string", "pattern": `^\d+$`},
-		LabelServiceExpose:                               map[string]interface{}{"type": "string"},
-		LabelServiceExposeTLSSecret:                      map[string]interface{}{"type": "string"},
-	},
-	"required": []string{
-		LabelWorkloadReplicas,
-		LabelWorkloadLivenessProbeType,
-	},
-	"allOf": []map[string]interface{}{
-		// Liveness probe
-		{
-			"oneOf": []map[string]interface{}{
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadLivenessProbeType: map[string]interface{}{
-							"const": "http",
-						},
-					},
-					"required": []string{LabelWorkloadLivenessProbeHTTPPath, LabelWorkloadLivenessProbeHTTPPort},
-				},
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadLivenessProbeType: map[string]interface{}{
-							"const": "tcp",
-						},
-					},
-					"required": []string{LabelWorkloadLivenessProbeTCPPort},
-				},
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadLivenessProbeType: map[string]interface{}{
-							"const": "exec",
-						},
-					},
-					"required": []string{LabelWorkloadLivenessProbeCommand},
-				},
-				// This acts as a catch all so should come in the end.
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadLivenessProbeType: map[string]interface{}{
-							"const": "none",
-						},
-					},
-				},
-			},
-		},
-		// Readiness probe
-		{
-			"anyOf": []map[string]interface{}{
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadReadinessProbeType: map[string]interface{}{
-							"const": "http",
-						},
-					},
-					"required": []string{LabelWorkloadReadinessProbeHTTPPath, LabelWorkloadReadinessProbeHTTPPort},
-				},
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadReadinessProbeType: map[string]interface{}{
-							"const": "tcp",
-						},
-					},
-					"required": []string{LabelWorkloadReadinessProbeTCPPort},
-				},
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadReadinessProbeType: map[string]interface{}{
-							"const": "exec",
-						},
-					},
-					"required": []string{LabelWorkloadReadinessProbeCommand},
-				},
-				// This acts as a catch all so should come in the end.
-				{
-					"properties": map[string]interface{}{
-						LabelWorkloadReadinessProbeType: map[string]interface{}{
-							"const": "none",
-						},
-					},
-				},
-			},
-		},
-	},
-	"additionalProperties": false,
-}
-
 // VolumesSchema defines validation rules for volume labels
 var VolumesSchema = map[string]interface{}{
 	"$schema": "http://json-schema.org/draft/2019-09/schema#",
diff --git a/pkg/kev/converter/kubernetes/project_service.go b/pkg/kev/converter/kubernetes/project_service.go
index aa7dccf..1c56563 100644
--- a/pkg/kev/converter/kubernetes/project_service.go
+++ b/pkg/kev/converter/kubernetes/project_service.go
@@ -32,42 +32,26 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 )
 
-// enabled returns Bool telling Kev whether app component is enabled/disabled
-func (p *ProjectService) enabled() bool {
-	if val, ok := p.Labels[config.LabelComponentEnabled]; ok {
-		if v, err := strconv.ParseBool(val); err == nil {
-			return v
-		}
-
-		log.WarnfWithFields(log.Fields{
-			"project-service": p.Name,
-			"enabled":         val,
-		}, "Unable to extract Bool value from %s label. Component will remain enabled.",
-			config.LabelComponentEnabled)
+func NewProjectService(svc composego.ServiceConfig) (ProjectService, error) {
+	cfg, err := config.K8SCfgFromCompose(&svc)
+	if err != nil {
+		return ProjectService{}, err
 	}
 
-	return true
+	return ProjectService{
+		ServiceConfig: svc,
+		K8SConfig:     cfg,
+	}, nil
+}
+
+// enabled returns Bool telling Kev whether app component is enabled/disabled
+func (p *ProjectService) enabled() bool {
+	return !p.K8SConfig.Disabled
 }
 
 // replicas returns number of replicas for given project service
 func (p *ProjectService) replicas() int32 {
-	if val, ok := p.Labels[config.LabelWorkloadReplicas]; ok {
-		replicas, err := strconv.Atoi(val)
-		if err != nil {
-			log.WarnfWithFields(log.Fields{
-				"project-service": p.Name,
-				"replicas":        val,
-			}, "Unable to extract integer value from %s label. Defaulting to 1 replica.",
-				config.LabelWorkloadReplicas)
-
-			return config.DefaultReplicaNumber
-		}
-		return int32(replicas)
-	} else if p.Deploy != nil && p.Deploy.Replicas != nil {
-		return int32(*p.Deploy.Replicas)
-	}
-
-	return config.DefaultReplicaNumber
+	return int32(p.K8SConfig.Workload.Replicas)
 }
 
 // autoscaleMaxReplicas returns maximum number of replicas for autoscaler
@@ -132,11 +116,7 @@ func (p *ProjectService) autoscaleTargetMemoryUtilization() int32 {
 
 // workloadType returns workload type for the project service
 func (p *ProjectService) workloadType() string {
-	workloadType := config.DefaultWorkload
-
-	if val, ok := p.Labels[config.LabelWorkloadType]; ok {
-		workloadType = val
-	}
+	workloadType := p.K8SConfig.Workload.Type
 
 	if p.Deploy != nil && p.Deploy.Mode == "global" && !strings.EqualFold(workloadType, config.DaemonsetWorkload) {
 		log.WarnfWithFields(log.Fields{
@@ -443,28 +423,8 @@ func (p *ProjectService) serviceAccountName() string {
 // restartPolicy return workload restart policy. Supports both docker-compose and Kubernetes notations.
 func (p *ProjectService) restartPolicy() v1.RestartPolicy {
 	policy := config.RestartPolicyAlways
-
-	// @step restart policy defined on the compose service
-	if len(p.Restart) > 0 {
-		policy = p.Restart
-	}
-
-	// @step restart policy defined in deploy block
-	if p.Deploy != nil && p.Deploy.RestartPolicy != nil {
-		policy = p.Deploy.RestartPolicy.Condition
-	}
-
-	if policy == "unless-stopped" {
-		log.WarnWithFields(log.Fields{
-			"project-service": p.Name,
-			"restart-policy":  policy,
-		}, "Restart policy 'unless-stopped' is not supported, converting it to 'always'")
-
-		policy = "always"
-	}
-
-	if val, ok := p.Labels[config.LabelWorkloadRestartPolicy]; ok {
-		policy = val
+	if p.K8SConfig.Workload.RestartPolicy != "" {
+		policy = p.K8SConfig.Workload.RestartPolicy
 	}
 
 	restartPolicy, err := getRestartPolicy(p.Name, policy)
@@ -549,7 +509,7 @@ func (p *ProjectService) ports() []composego.ServicePortConfig {
 }
 
 func (p *ProjectService) LivenessProbe() (*v1.Probe, error) {
-	p1 := composego.ServiceConfig(*p)
+	p1 := composego.ServiceConfig(p.ServiceConfig)
 	k8sconf, err := config.K8SCfgFromCompose(&p1)
 	if err != nil {
 		return nil, err
@@ -559,7 +519,7 @@ func (p *ProjectService) LivenessProbe() (*v1.Probe, error) {
 }
 
 func (p *ProjectService) ReadinessProbe() (*v1.Probe, error) {
-	p1 := composego.ServiceConfig(*p)
+	p1 := composego.ServiceConfig(p.ServiceConfig)
 	k8sconf, err := config.K8SCfgFromCompose(&p1)
 	if err != nil {
 		return nil, err
diff --git a/pkg/kev/converter/kubernetes/transform.go b/pkg/kev/converter/kubernetes/transform.go
index 2b1b97b..2d8e7ca 100644
--- a/pkg/kev/converter/kubernetes/transform.go
+++ b/pkg/kev/converter/kubernetes/transform.go
@@ -99,7 +99,10 @@ func (k *Kubernetes) Transform() ([]runtime.Object, error) {
 		stepSvc := sg.Add(fmt.Sprintf("Converting service: %s", pSvc.Name))
 		var objects []runtime.Object
 
-		projectService := ProjectService(pSvc)
+		projectService, err := NewProjectService(pSvc)
+		if err != nil {
+			return nil, err
+		}
 
 		// @step skip disabled services
 		if !projectService.enabled() {
@@ -689,7 +692,6 @@ func (k *Kubernetes) initIngress(projectService ProjectService, port int32) *net
 
 // initHpa intialised horizontal pod autoscaler for a project service
 func (k *Kubernetes) initHpa(projectService ProjectService, target runtime.Object) *autoscalingv2beta2.HorizontalPodAutoscaler {
-
 	t := reflect.ValueOf(target).Elem()
 	typeMeta := t.FieldByName("TypeMeta").Interface().(meta.TypeMeta)
 	if !contains([]string{"Deployment", "StatefulSet"}, typeMeta.Kind) {
diff --git a/pkg/kev/converter/kubernetes/types.go b/pkg/kev/converter/kubernetes/types.go
index bd331f5..4a29372 100644
--- a/pkg/kev/converter/kubernetes/types.go
+++ b/pkg/kev/converter/kubernetes/types.go
@@ -21,6 +21,7 @@
 package kubernetes
 
 import (
+	"github.com/appvia/kev/pkg/kev/config"
 	composego "github.com/compose-spec/compose-go/types"
 )
 
@@ -52,4 +53,7 @@ type Volumes struct {
 }
 
 // ProjectService is a wrapper type around composego.ServiceConfig
-type ProjectService composego.ServiceConfig
+type ProjectService struct {
+	composego.ServiceConfig
+	K8SConfig config.K8SConfiguration
+}
diff --git a/pkg/kev/services.go b/pkg/kev/services.go
index 8ff0a64..f67963c 100644
--- a/pkg/kev/services.go
+++ b/pkg/kev/services.go
@@ -18,18 +18,15 @@ package kev
 
 import (
 	"encoding/json"
-	"errors"
 	"regexp"
-	"strings"
 
 	"github.com/appvia/kev/pkg/kev/config"
 	composego "github.com/compose-spec/compose-go/types"
-	"github.com/xeipuuv/gojsonschema"
 )
 
 func newServiceConfig(s composego.ServiceConfig) (ServiceConfig, error) {
 	config := ServiceConfig{Name: s.Name, Labels: s.Labels, Environment: s.Environment, Extensions: s.Extensions}
-	return config, config.validate()
+	return config, nil
 }
 
 // MarshalYAML makes Services implement yaml.Marshaller.
@@ -93,47 +90,6 @@ func (sc ServiceConfig) detectSecretsInEnvVars(matchers []map[string]string) []s
 	return matches
 }
 
-// validate runs validation of kev labels against config schema validation.
-// IMPORTANT: All labels prefixed with `kev.` are always validated!
-//            Other labels are left as is and won't be validated!
-// NOTE: non kev labels are normally converted to annotations which is handy
-// with things like ingress objects accepting a number of custom annotations.
-// @todo(mc): post migrating labels to extensions this could be addressed by
-// dedicated `annotations` section.
-func (sc ServiceConfig) validate() error {
-	svcLabelsToValidate := composego.Labels{}
-	for slk, slv := range sc.Labels {
-		if strings.HasPrefix(slk, config.LabelPrefix) {
-			svcLabelsToValidate[slk] = slv
-		}
-	}
-
-	ls := gojsonschema.NewGoLoader(config.ServicesSchema)
-	ld := gojsonschema.NewGoLoader(svcLabelsToValidate)
-
-	result, err := gojsonschema.Validate(ls, ld)
-	if err != nil {
-		return err
-	}
-
-	if result.Valid() {
-		return nil
-	}
-
-	// Prioritise clear error messages.
-	if e := findError(result, withType("required")); e != nil {
-		return errors.New(e.Description())
-	}
-
-	// Exclude errors that are very cryptic and hurt usability.
-	if e := findError(result, excludeTypes("number_one_of", "number_any_of", "number_all_of")); e != nil {
-		return errors.New(e.Description())
-	}
-
-	// If we don't find anything useful just go with whatever is available.
-	return errors.New(result.Errors()[0].Description())
-}
-
 // GetLabels gets a service's labels
 func (sc ServiceConfig) GetLabels() map[string]string {
 	return sc.Labels


Test patch:
diff --git a/pkg/kev/converter/kubernetes/helpers_test.go b/pkg/kev/converter/kubernetes/helpers_test.go
index dd25769..c2f4398 100644
--- a/pkg/kev/converter/kubernetes/helpers_test.go
+++ b/pkg/kev/converter/kubernetes/helpers_test.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 
 	"github.com/appvia/kev/pkg/kev/log"
+	"github.com/google/go-cmp/cmp"
 	"github.com/sirupsen/logrus"
 	"github.com/sirupsen/logrus/hooks/test"
 
@@ -37,7 +38,7 @@ func init() {
 
 func assertLog(level logrus.Level, message string, fields map[string]string) {
 	Expect(hook.LastEntry().Level).To(Equal(level))
-	Expect(hook.LastEntry().Message).To(Equal(message))
+	Expect(cmp.Diff(hook.LastEntry().Message, message)).To(BeEmpty())
 	for k, v := range fields {
 		Expect(hook.LastEntry().Data).To(HaveKeyWithValue(k, v))
 	}
diff --git a/pkg/kev/converter/kubernetes/project_service_test.go b/pkg/kev/converter/kubernetes/project_service_test.go
index 204e9d2..f39f6f7 100644
--- a/pkg/kev/converter/kubernetes/project_service_test.go
+++ b/pkg/kev/converter/kubernetes/project_service_test.go
@@ -64,7 +64,7 @@ var _ = Describe("ProjectService", func() {
 		healthcheck = composego.HealthCheckConfig{}
 		projectVolumes = composego.Volumes{}
 
-		k8sconf = config.DefaultK8SConfig()
+		k8sconf = config.K8SConfiguration{}
 	})
 
 	JustBeforeEach(func() {
@@ -74,7 +74,7 @@ var _ = Describe("ProjectService", func() {
 			config.K8SExtensionKey: ext,
 		}
 
-		projectService = ProjectService{
+		projectService, err = NewProjectService(composego.ServiceConfig{
 			Name:        projectServiceName,
 			Labels:      labels,
 			Deploy:      deploy,
@@ -84,10 +84,11 @@ var _ = Describe("ProjectService", func() {
 			HealthCheck: &healthcheck,
 			Volumes:     volumes,
 			Extensions:  extensions,
-		}
+		})
+		Expect(err).NotTo(HaveOccurred())
 
 		services := composego.Services{}
-		services = append(services, composego.ServiceConfig(projectService))
+		services = append(services, projectService.ServiceConfig)
 
 		project = composego.Project{
 			Volumes:    projectVolumes,
@@ -97,45 +98,28 @@ var _ = Describe("ProjectService", func() {
 	})
 
 	Describe("enabled", func() {
-
-		When("component toggle label is set to Truthy value", func() {
+		When("component toggle extension is set to disable=true", func() {
 			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelComponentEnabled: "true",
-				}
+				k8sconf.Disabled = true
 			})
 
 			It("returns true", func() {
-				Expect(projectService.enabled()).To(BeTrue())
-			})
-		})
-
-		When("component toggle label is set to Falsy value", func() {
-			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelComponentEnabled: "false",
-				}
-			})
-
-			It("returns false", func() {
 				Expect(projectService.enabled()).To(BeFalse())
 			})
 		})
 
-		When("component toggle label is set to any string value not representing a boolean value (one of: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.)", func() {
+		When("component toggle extension to set disable=false", func() {
 			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelComponentEnabled: "anyrandomstring",
-				}
+				k8sconf.Disabled = false
 			})
 
-			It("returns true", func() {
+			It("returns false", func() {
 				Expect(projectService.enabled()).To(BeTrue())
 			})
 		})
 
-		When("component toggle label is not specified", func() {
-			It("defaults to true if no label", func() {
+		When("component toggle extension is not specified", func() {
+			It("defaults to true", func() {
 				Expect(projectService.enabled()).To(BeTrue())
 			})
 		})
@@ -145,12 +129,10 @@ var _ = Describe("ProjectService", func() {
 
 		replicas := 10
 
-		Context("when provided via label", func() {
+		Context("when provided via extension", func() {
 
 			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelWorkloadReplicas: strconv.Itoa(replicas),
-				}
+				k8sconf.Workload.Replicas = replicas
 			})
 
 			It("will use a label value", func() {
@@ -158,12 +140,10 @@ var _ = Describe("ProjectService", func() {
 			})
 		})
 
-		Context("when provided via both the label and as part of the project service spec", func() {
+		Context("when provided via both the extension and as part of the project service spec", func() {
 
 			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelWorkloadReplicas: strconv.Itoa(replicas),
-				}
+				k8sconf.Workload.Replicas = replicas
 
 				deployBlockReplicas := uint64(2)
 				deploy = &composego.DeployConfig{
@@ -176,8 +156,7 @@ var _ = Describe("ProjectService", func() {
 			})
 		})
 
-		Context("when replicas label not present but specified as part of the project service spec", func() {
-
+		Context("when replicas extension not present but specified as part of the project service spec", func() {
 			replicas := uint64(2)
 
 			BeforeEach(func() {
@@ -187,11 +166,12 @@ var _ = Describe("ProjectService", func() {
 			})
 
 			It("will use a replica number as specified in deploy block", func() {
+				Expect(projectService.Deploy.Replicas).NotTo(BeNil())
 				Expect(projectService.replicas()).To(BeEquivalentTo(replicas))
 			})
 		})
 
-		Context("when there is no replicas label supplied nor deploy block contains number of replicas", func() {
+		Context("when there is no replicas extensions supplied nor deploy block contains number of replicas", func() {
 			It("will use default number of replicas", func() {
 				Expect(projectService.replicas()).To(BeEquivalentTo(config.DefaultReplicaNumber))
 			})
@@ -273,13 +253,11 @@ var _ = Describe("ProjectService", func() {
 
 			workloadType := "StatefulSet"
 
-			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelWorkloadType: workloadType,
-				}
+			JustBeforeEach(func() {
+				projectService.K8SConfig.Workload.Type = workloadType
 			})
 
-			It("will use a label value", func() {
+			It("will use a extension value", func() {
 				Expect(projectService.workloadType()).To(Equal(workloadType))
 			})
 		})
@@ -291,20 +269,29 @@ var _ = Describe("ProjectService", func() {
 		})
 
 		Context("when deploy block `mode` defined as `global` and workload type is different than DaemonSet", func() {
-
 			projectWorkloadType := config.StatefulsetWorkload
 
-			BeforeEach(func() {
-				labels = composego.Labels{
-					config.LabelWorkloadType: projectWorkloadType,
-				}
+			JustBeforeEach(func() {
+				projectService.K8SConfig.Workload.Type = projectWorkloadType
+				m, err := projectService.K8SConfig.ToMap()
+				Expect(err).NotTo(HaveOccurred())
 
-				deploy = &composego.DeployConfig{
+				svc := projectService.ServiceConfig
+				svc.Deploy = &composego.DeployConfig{
 					Mode: "global",
 				}
+				svc.Extensions = map[string]interface{}{
+					config.K8SExtensionKey: m,
+				}
+
+				projectService, err = NewProjectService(svc)
+				Expect(err).NotTo(HaveOccurred())
 			})
 
 			It("warns the user about the mismatch", func() {
+				Expect(projectService.K8SConfig.Workload.Type).To(Equal(projectWorkloadType))
+				Expect(projectService.Deploy.Mode).To(Equal("global"))
+
 				projectService.workloadType()
 				assertLog(logrus.WarnLevel,
 					"Compose service defined as 'global' should map to K8s DaemonSet. Current configuration forces conversion to StatefulSet",
@@ -978,8 +965,14 @@ var _ = Describe("ProjectService", func() {
 			Context("and defined on the project service directly with no deploy block", func() {
 				policy := config.RestartPolicyNever
 
-				It("returns restart policy defined on the project service level", func() {
+				JustBeforeEach(func() {
 					projectService.Restart = policy
+					ps, err := NewProjectService(projectService.ServiceConfig)
+					Expect(err).NotTo(HaveOccurred())
+					projectService = ps
+				})
+
+				It("returns restart policy defined on the project service level", func() {
 					Expect(projectService.restartPolicy()).To(Equal(v1.RestartPolicy(policy)))
 				})
 			})
@@ -1011,8 +1004,7 @@ var _ = Describe("ProjectService", func() {
 					assertLog(logrus.WarnLevel,
 						"Restart policy 'unless-stopped' is not supported, converting it to 'always'",
 						map[string]string{
-							"project-service": projectServiceName,
-							"restart-policy":  policy,
+							"restart-policy": policy,
 						},
 					)
 				})
@@ -1022,10 +1014,19 @@ var _ = Describe("ProjectService", func() {
 			Context("when invalid policy has been provided", func() {
 				policy := "invalid-policy"
 
-				BeforeEach(func() {
-					labels = composego.Labels{
-						config.LabelWorkloadRestartPolicy: policy,
+				JustBeforeEach(func() {
+					projectService.K8SConfig.Workload.RestartPolicy = policy
+					m, err := projectService.K8SConfig.ToMap()
+					Expect(err).NotTo(HaveOccurred())
+
+					svc := projectService.ServiceConfig
+					svc.Extensions = map[string]interface{}{
+						config.K8SExtensionKey: m,
 					}
+
+					projectService, err = NewProjectService(svc)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(projectService.K8SConfig.Workload.RestartPolicy).To(Equal(policy))
 				})
 
 				It("warns the user and defaults restart policy to `Always`", func() {
@@ -1158,7 +1159,6 @@ var _ = Describe("ProjectService", func() {
 			retries := uint64(3)
 
 			BeforeEach(func() {
-				labels.Add(config.LabelWorkloadLivenessProbeType, config.ProbeTypeExec.String())
 				healthcheck = composego.HealthCheckConfig{
 					Test: composego.HealthCheckTest{
 						"CMD-SHELL",
@@ -1177,12 +1177,12 @@ var _ = Describe("ProjectService", func() {
 				Expect(cmp.Diff(result, &v1.Probe{
 					Handler: v1.Handler{
 						Exec: &v1.ExecAction{
-							Command: config.DefaultLivenessProbeCommand,
+							Command: []string{"my command"},
 						},
 					},
 					TimeoutSeconds:      10,
-					PeriodSeconds:       60,
-					InitialDelaySeconds: 60,
+					PeriodSeconds:       10,
+					InitialDelaySeconds: 10,
 					FailureThreshold:    3,
 					SuccessThreshold:    3,
 				})).To(BeEmpty())
diff --git a/pkg/kev/converter/kubernetes/transform_test.go b/pkg/kev/converter/kubernetes/transform_test.go
index 5ed4f93..c0c4159 100644
--- a/pkg/kev/converter/kubernetes/transform_test.go
+++ b/pkg/kev/converter/kubernetes/transform_test.go
@@ -54,15 +54,17 @@ var _ = Describe("Transform", func() {
 			Services: composego.Services{},
 		}
 
-		projectService = ProjectService{
+		ps, err := NewProjectService(composego.ServiceConfig{
 			Name:       "web",
 			Image:      "some-image",
 			Extensions: extensions,
-		}
+		})
+		Expect(err).NotTo(HaveOccurred())
+		projectService = ps
 	})
 
 	JustBeforeEach(func() {
-		project.Services = append(project.Services, composego.ServiceConfig(projectService))
+		project.Services = append(project.Services, projectService.ServiceConfig)
 
 		k = Kubernetes{
 			Opt:      ConvertOptions{},
@@ -113,9 +115,11 @@ var _ = Describe("Transform", func() {
 		When("project service doesn't have image specified", func() {
 
 			BeforeEach(func() {
-				projectService = ProjectService{
+				ps, err := NewProjectService(composego.ServiceConfig{
 					Name: "web",
-				}
+				})
+				Expect(err).NotTo(HaveOccurred())
+				projectService = ps
 			})
 
 			It("uses project service name as service image", func() {
@@ -979,10 +983,10 @@ var _ = Describe("Transform", func() {
 					})
 
 					When("autoscaling max replicas number is lower or equal to initial number of replicas", func() {
-
 						BeforeEach(func() {
+							projectService.K8SConfig.Workload.Replicas = 10
+
 							projectService.Labels = composego.Labels{
-								config.LabelWorkloadReplicas:             "10",
 								config.LabelWorkloadAutoscaleMaxReplicas: "5",
 							}
 						})
diff --git a/pkg/kev/converter/kubernetes/utils_test.go b/pkg/kev/converter/kubernetes/utils_test.go
index 68acfd9..ee4a0f6 100644
--- a/pkg/kev/converter/kubernetes/utils_test.go
+++ b/pkg/kev/converter/kubernetes/utils_test.go
@@ -190,17 +190,20 @@ var _ = Describe("Utils", func() {
 	})
 
 	Describe("sortServices", func() {
-		s1 := ProjectService{Name: "z"}
-		s2 := ProjectService{Name: "a"}
-		s3 := ProjectService{Name: "c"}
+		s1, err := NewProjectService(composego.ServiceConfig{Name: "z"})
+		Expect(err).NotTo(HaveOccurred())
+		s2, err := NewProjectService(composego.ServiceConfig{Name: "a"})
+		Expect(err).NotTo(HaveOccurred())
+		s3, err := NewProjectService(composego.ServiceConfig{Name: "c"})
+		Expect(err).NotTo(HaveOccurred())
 
 		services := composego.Services{}
 
 		p := composego.Project{
 			Services: append(services,
-				composego.ServiceConfig(s1),
-				composego.ServiceConfig(s2),
-				composego.ServiceConfig(s3),
+				composego.ServiceConfig(s1.ServiceConfig),
+				composego.ServiceConfig(s2.ServiceConfig),
+				composego.ServiceConfig(s3.ServiceConfig),
 			),
 		}
 
@@ -240,13 +243,14 @@ var _ = Describe("Utils", func() {
 		networkNameA := "mynetA"
 		networkNameB := "mynetB"
 
-		projectService := ProjectService{
+		projectService, err := NewProjectService(composego.ServiceConfig{
 			Name: svcName,
 			Networks: map[string]*composego.ServiceNetworkConfig{
 				networkNameA: {},
 				networkNameB: {},
 			},
-		}
+		})
+		Expect(err).NotTo(HaveOccurred())
 
 		It("prepares template metadata labels with service and network policy selectors", func() {
 			l := configLabelsWithNetwork(projectService)
@@ -274,7 +278,7 @@ var _ = Describe("Utils", func() {
 		})
 
 		Context("when project services contain named service", func() {
-			s := ProjectService{
+			s, err := NewProjectService(composego.ServiceConfig{
 				Name: "db",
 				Volumes: []composego.ServiceVolumeConfig{
 					{
@@ -283,10 +287,11 @@ var _ = Describe("Utils", func() {
 						ReadOnly: false,
 					},
 				},
-			}
+			})
+			Expect(err).NotTo(HaveOccurred())
 
 			JustBeforeEach(func() {
-				project.Services = append(project.Services, composego.ServiceConfig(s))
+				project.Services = append(project.Services, s.ServiceConfig)
 			})
 
 			Context("and project service doesn't reference volumes from other project services (no VolumesFrom present)", func() {
@@ -300,7 +305,7 @@ var _ = Describe("Utils", func() {
 
 				Context("and the mount path for project volume and dependent volume are the same", func() {
 
-					s2 := ProjectService{
+					s2, err := NewProjectService(composego.ServiceConfig{
 						Name: "other",
 						Volumes: []composego.ServiceVolumeConfig{
 							{
@@ -310,12 +315,13 @@ var _ = Describe("Utils", func() {
 							},
 						},
 						VolumesFrom: []string{s.Name},
-					}
+					})
+					Expect(err).NotTo(HaveOccurred())
 
 					JustBeforeEach(func() {
 						project.Services = append(project.Services,
-							composego.ServiceConfig(s),
-							composego.ServiceConfig(s2),
+							s.ServiceConfig,
+							s2.ServiceConfig,
 						)
 					})
 
@@ -327,7 +333,7 @@ var _ = Describe("Utils", func() {
 
 				Context("and the mount path for project volume and dependent volume are different", func() {
 
-					s2 := ProjectService{
+					s2, err := NewProjectService(composego.ServiceConfig{
 						Name: "other",
 						Volumes: []composego.ServiceVolumeConfig{
 							{
@@ -337,12 +343,13 @@ var _ = Describe("Utils", func() {
 							},
 						},
 						VolumesFrom: []string{s.Name},
-					}
+					})
+					Expect(err).NotTo(HaveOccurred())
 
 					JustBeforeEach(func() {
 						project.Services = append(project.Services,
-							composego.ServiceConfig(s),
-							composego.ServiceConfig(s2),
+							s.ServiceConfig,
+							s2.ServiceConfig,
 						)
 					})
 
@@ -571,9 +578,10 @@ var _ = Describe("Utils", func() {
 
 	Describe("configAllLabels", func() {
 		svcName := "db"
-		projectService := ProjectService{
+		projectService, err := NewProjectService(composego.ServiceConfig{
 			Name: svcName,
-		}
+		})
+		Expect(err).NotTo(HaveOccurred())
 
 		Context("without any labels defined in deploy block", func() {
 			It("returns a map of labels containing selector only", func() {
@@ -599,12 +607,13 @@ var _ = Describe("Utils", func() {
 		Context("with project service labels present", func() {
 
 			Context("containing kev specific labels", func() {
-				projectService := ProjectService{
+				projectService, err := NewProjectService(composego.ServiceConfig{
 					Labels: composego.Labels{
 						"FOO":                   "BAR",
 						config.LabelServiceType: "value",
 					},
-				}
+				})
+				Expect(err).NotTo(HaveOccurred())
 
 				It("returns a map of labels excluding kev control labels", func() {
 					Expect(configAnnotations(projectService)).To(HaveLen(1))
@@ -613,12 +622,13 @@ var _ = Describe("Utils", func() {
 			})
 
 			Context("not containing kev specific labels", func() {
-				projectService := ProjectService{
+				projectService, err := NewProjectService(composego.ServiceConfig{
 					Labels: composego.Labels{
 						"FOO": "BAR",
 						"BAR": "BAZ",
 					},
-				}
+				})
+				Expect(err).NotTo(HaveOccurred())
 
 				It("returns a map of labels as expected", func() {
 					Expect(configAnnotations(projectService)).To(HaveLen(2))
diff --git a/pkg/kev/kev_reconcile_test.go b/pkg/kev/kev_reconcile_test.go
index 6d24abc..f64b94f 100644
--- a/pkg/kev/kev_reconcile_test.go
+++ b/pkg/kev/kev_reconcile_test.go
@@ -270,7 +270,8 @@ var _ = Describe("Reconcile", func() {
 				It("should configure parse config into extensions", func() {
 					expected, err := newDefaultServiceExtensions("wordpress", config.K8SConfiguration{
 						Workload: config.Workload{
-							Replicas: 3,
+							Replicas:      3,
+							RestartPolicy: "always",
 						},
 						Service: config.Service{
 							Type: config.ClusterIPService,
@@ -293,6 +294,9 @@ var _ = Describe("Reconcile", func() {
 
 				It("should configure the added service extensions from healthcheck config", func() {
 					expected, err := newDefaultServiceExtensions("wordpress", config.K8SConfiguration{
+						Workload: config.Workload{
+							RestartPolicy: "always",
+						},
 						Service: config.Service{
 							Type: config.ClusterIPService,
 						},
@@ -528,12 +532,13 @@ var _ = Describe("Reconcile", func() {
 
 func newDefaultServiceExtensions(name string, k8sconfs ...config.K8SConfiguration) (map[string]interface{}, error) {
 	k8s := config.K8SConfiguration{
-		Enabled: true,
+		Disabled: false,
 		Workload: config.Workload{
 			LivenessProbe:  config.DefaultLivenessProbe(),
 			ReadinessProbe: config.DefaultReadinessProbe(),
 			Type:           config.DefaultWorkload,
 			Replicas:       config.DefaultReplicaNumber,
+			RestartPolicy:  "always",
 		},
 	}
 
diff --git a/pkg/kev/services_test.go b/pkg/kev/services_test.go
deleted file mode 100644
index 89a5c0f..0000000
--- a/pkg/kev/services_test.go
+++ /dev/null
@@ -1,330 +0,0 @@
-/**
- * Copyright 2020 Appvia Ltd <info@appvia.io>
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package kev
-
-import (
-	"github.com/appvia/kev/pkg/kev/config"
-
-	composego "github.com/compose-spec/compose-go/types"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-var _ = Describe("ServiceConfig", func() {
-	Context("validation", func() {
-
-		When("base labels", func() {
-
-			// TODO: This needs a better test in a future iteration, the base labels are no longer just the required ones.
-			It("fails when base labels are not present", func() {
-				err := ServiceConfig{Labels: composego.Labels{
-					config.LabelWorkloadReplicas: "1",
-				}}.validate()
-				Expect(err).Should(MatchError(ContainSubstring(config.LabelWorkloadLivenessProbeType)))
-
-				err = ServiceConfig{Labels: composego.Labels{
-					config.LabelWorkloadLivenessProbeType:    config.ProbeTypeExec.String(),
-					config.LabelWorkloadLivenessProbeCommand: "echo i'm a useless probe",
-				}}.validate()
-				Expect(err).Should(MatchError(ContainSubstring(config.LabelWorkloadReplicas)))
-
-				err = ServiceConfig{Labels: composego.Labels{}}.validate()
-				Expect(err).Should(HaveOccurred())
-			})
-
-			It("success if the necessary labels are present", func() {
-				err := ServiceConfig{Labels: composego.Labels{
-					config.LabelWorkloadLivenessProbeType:    config.ProbeTypeExec.String(),
-					config.LabelWorkloadLivenessProbeCommand: "echo i'm a useless probe",
-					config.LabelWorkloadReplicas:             "1",
-				}}.validate()
-				Expect(err).NotTo(HaveOccurred())
-			})
-
-			It("fails when replicas label is not a number", func() {
-				serviceConfig := ServiceConfig{Labels: composego.Labels{config.LabelWorkloadLivenessProbeCommand: "value"}}
-				serviceConfig.Labels[config.LabelWorkloadReplicas] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-		})
-
-		When("other labels", func() {
-			var serviceConfig ServiceConfig
-
-			BeforeEach(func() {
-				serviceConfig = ServiceConfig{
-					Labels: composego.Labels{
-						config.LabelWorkloadLivenessProbeType: config.ProbeTypeNone.String(),
-						config.LabelWorkloadReplicas:          "1",
-					}}
-			})
-
-			It("fails when unknown labels are present", func() {
-				serviceConfig.Labels["kev.workload.unknown"] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("ignores non-kev labels and doesn't validate them", func() {
-				serviceConfig.Labels["kubernetes.io/ingress.class"] = "external"
-				Expect(serviceConfig.validate()).Should(Not(HaveOccurred()))
-			})
-
-			It("fails when component enabled label has incorrect value", func() {
-				serviceConfig.Labels[config.LabelComponentEnabled] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when image pull policy label has wrong value", func() {
-				serviceConfig.Labels[config.LabelWorkloadImagePullPolicy] = "wrong-value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when liveness probe disabled label has wrong value", func() {
-				serviceConfig.Labels[config.LabelWorkloadImagePullPolicy] = "wrong-value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when liveness probe initial delay label is not a duration", func() {
-				serviceConfig.Labels[config.LabelWorkloadLivenessProbeInitialDelay] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when liveness probe initial interval label is not a duration", func() {
-				serviceConfig.Labels[config.LabelWorkloadLivenessProbeInterval] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when liveness probe retries label is not a number", func() {
-				serviceConfig.Labels[config.LabelWorkloadLivenessProbeRetries] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when liveness probe timeout label is not a duration", func() {
-				serviceConfig.Labels[config.LabelWorkloadLivenessProbeTimeout] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when max memory label is not a memory unit", func() {
-				serviceConfig.Labels[config.LabelWorkloadMaxMemory] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when the memory label is not a memory unit", func() {
-				serviceConfig.Labels[config.LabelWorkloadMemory] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when rolling update max surge label is not a number", func() {
-				serviceConfig.Labels[config.LabelWorkloadRollingUpdateMaxSurge] = "value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when service account name label has the incorrect pattern", func() {
-				serviceConfig.Labels[config.LabelWorkloadServiceAccountName] = " wrong-pattern"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			It("fails when workload type label has the wrong value", func() {
-				serviceConfig.Labels[config.LabelWorkloadType] = "wrong-value"
-				Expect(serviceConfig.validate()).Should(HaveOccurred())
-			})
-
-			Context("http probe validation", func() {
-				Context("readiness", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeNone.String()
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeType] = config.ProbeTypeHTTP.String()
-					})
-
-					It("fails when http probe has missing path", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeHTTPPort] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadReadinessProbeHTTPPath))
-					})
-
-					It("fails when http probe has missing port", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeHTTPPath] = "/status"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadReadinessProbeHTTPPort))
-					})
-
-					It("succeeds when all values are provided", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeHTTPPath] = "/status"
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeHTTPPort] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-
-				Context("liveness", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeHTTP.String()
-					})
-
-					It("fails when http probe has missing path", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeHTTPPort] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadLivenessProbeHTTPPath))
-					})
-
-					It("fails when http probe has missing port", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeHTTPPath] = "/status"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadLivenessProbeHTTPPort))
-					})
-
-					It("succeeds when all values are provided", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeHTTPPort] = "/status"
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeHTTPPath] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-			})
-
-			Context("tcp probe validation", func() {
-				Context("readiness", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeNone.String()
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeType] = config.ProbeTypeTCP.String()
-					})
-
-					It("fails when tcp probe has missing port", func() {
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadReadinessProbeTCPPort))
-					})
-
-					It("succeeds when port is provided", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeTCPPort] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-				Context("liveness", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeTCP.String()
-					})
-
-					It("fails when tcp probe has missing port", func() {
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadLivenessProbeTCPPort))
-					})
-
-					It("succeeds when port is provided", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeTCPPort] = "8080"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-			})
-
-			Context("command probe validation", func() {
-				Context("rediness probes", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeNone.String()
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeType] = config.ProbeTypeExec.String()
-					})
-
-					It("fails when command probe has missing properties", func() {
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadReadinessProbeCommand))
-					})
-				})
-
-				Context("liveness probe", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeExec.String()
-					})
-
-					It("fails when command probe has missing properties", func() {
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring("required"))
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadLivenessProbeCommand))
-					})
-				})
-			})
-
-			Context("probe type enum validation", func() {
-				Context("readiness", func() {
-					BeforeEach(func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeNone.String()
-					})
-
-					It("fails when type is misspelled", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeType] = "wrong-value"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadReadinessProbeType))
-						Expect(err.Error()).To(ContainSubstring("none"))
-					})
-
-					It("succeeds if set to none", func() {
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeType] = config.ProbeTypeNone.String()
-						serviceConfig.Labels[config.LabelWorkloadReadinessProbeCommand] = "i'm just a leftover"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-
-				Context("liveness", func() {
-					It("fails when type is misspelled", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = "wrong-value"
-
-						err := serviceConfig.validate()
-						Expect(err).To(HaveOccurred())
-						Expect(err.Error()).To(ContainSubstring(config.LabelWorkloadLivenessProbeType))
-						Expect(err.Error()).To(ContainSubstring("none"))
-					})
-
-					It("succeeds if set to none", func() {
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeType] = config.ProbeTypeNone.String()
-						serviceConfig.Labels[config.LabelWorkloadLivenessProbeCommand] = "i'm just a leftover"
-
-						err := serviceConfig.validate()
-						Expect(err).To(Succeed())
-					})
-				})
-			})
-
-		})
-	})
-})
diff --git a/pkg/kev/testdata/init-default/compose-yaml/output.yaml b/pkg/kev/testdata/init-default/compose-yaml/output.yaml
index 3f3e346..18597b8 100644
--- a/pkg/kev/testdata/init-default/compose-yaml/output.yaml
+++ b/pkg/kev/testdata/init-default/compose-yaml/output.yaml
@@ -2,7 +2,7 @@ version: "3.9"
 services:
   db:
     x-k8s:
-      enabled: true
+      disabled: false
       service:
         type: None
       workload:
@@ -23,6 +23,7 @@ services:
           timeout: 10s
           type: none
         replicas: 1
+        restartPolicy: always
         type: StatefulSet
 volumes:
   db_data: