Repository: appvia/kev
Pull request #566 (2021-06-11 11:01:05)

Add storage resource params
Resolves: #564 

This PR adds the additional configuration parameters around workload resource requests/limits. Specifically for ephemeral storage that workload can reserve and its upper limit:

```yaml
x-k8s:
  workload:
    resource:
      storage: 1G
      maxStorage: 2G
```

You can express storage as a plain integer or as a fixed-point number using one of these suffixes: E, P, T, G, M, K. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki.

By default `storage`/`maxStorage` aren't specified and won't be included. Only when explicitly defined those parameters will be included in resource requests and limits. 


Patch:
diff --git a/docs/reference/config-example.md b/docs/reference/config-example.md
index e8f6a8b..ec42e38 100644
--- a/docs/reference/config-example.md
+++ b/docs/reference/config-example.md
@@ -11,7 +11,7 @@ version: "3.7"
 services:                                                # compose services section
   wordpress:                                             # compose project service name
     x-k8s:                                               # compose K8s configuration extension
-      disabled: false                                    # When disabled the compose service won't be included in generated K8s manifests. Defaults to 'false'. 
+      disabled: false                                    # When disabled the compose service won't be included in generated K8s manifests. Defaults to 'false'.
       service:                                           # K8s service configuration (only required if values are overridden)
         type: None                                       # Default: none (no service). Possible options: none | headless | clusterip | nodeport | loadbalancer.
         nodeport:                                        # Default: nil. Set with numeric value e.g. 5555. Only taken into account when working with service.type: nodeport
@@ -29,7 +29,7 @@ services:                                                # compose services sect
         livenessProbe:                                   # Workload's liveness probe
           ### EXEC
           type: exec                                     # Default: exec. Possible options: none | exec | http | tcp. See examples for other probe types in commented sections below.
-          exec:                                          # The exec command matching the liveness probe type.      
+          exec:                                          # The exec command matching the liveness probe type.
             command:                                     # Liveness probe command to run.
               - echo
               - Define healthcheck command for service
@@ -72,8 +72,10 @@ services:                                                # compose services sect
         resource:                                        # Defines resource requests and limits
           cpu: "0.1"                                     # Default: 0.1. CPU request per workload.
           maxCpu: "0.5"                                  # Default: 0.5. CPU limit per workload.
-          maxMemory: 500Mi                               # Default: 500Mi. Memory limit per workload.              
           memory: 10Mi                                   # Default: 10Mi. Memory request per workload.
+          maxMemory: 500Mi                               # Default: 500Mi. Memory limit per workload.
+          storage: 100Mi                                 # Default: not set. Ephemeral storage size request per workload.
+          maxStorage: 500Mi                              # Default: not set. Ephemeral storage size limit per workload.
         restartPolicy: Always                            # Default: Always. Possible options: Always / OnFailure / Never.
         rollingUpdateMaxSurge: 1                         # Default: 1. Maximum number of containers to be updated at a time.
         serviceAccountName: default                      # Default: default. Service account name to be used.
diff --git a/docs/reference/config-params.md b/docs/reference/config-params.md
index 38d1896..4fd99df 100644
--- a/docs/reference/config-params.md
+++ b/docs/reference/config-params.md
@@ -11,13 +11,13 @@ Kev leverages the Docker Compose specification to configure and prepare an appli
 
 Project wide configuration is OPTIONAL and can be defined in one (or more) of the source compose files used to initialise the project.
 
-It will be applied against all environments unless a specific environment overrides a setting with its own value. 
+It will be applied against all environments unless a specific environment overrides a setting with its own value.
 
 ## Environment configuration
 
 Environment configuration lives in a dedicated docker compose override file. This automatically gets applied to the project's source docker compose files at the `render` phase.
 
-Any project wide configuration found will be overridden by environment specific values.  
+Any project wide configuration found will be overridden by environment specific values.
 
 ### Component level configuration
 
@@ -74,7 +74,7 @@ services:
   my-service:
     x-k8s:
       workload:
-        imagePull: 
+        imagePull:
           policy: IfNotPresent
 ...
 ```
@@ -443,6 +443,45 @@ services:
           maxMemory: 0.3Gi
 ...
 ```
+### workload.resource.storage
+
+Defines the ephemeral storage size request for a given workload. See official K8s [documentation](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#setting-requests-and-limits-for-local-ephemeral-storage).
+
+#### Default: `nil` (not set)
+
+#### Possible options: Arbitrary [Memory units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory). Examples: `64Mi`, `1Gi`...
+
+> workload.resource.storage:
+```yaml
+version: 3.7
+services:
+  my-service:
+    x-k8s:
+      workload:
+        resource:
+          storage: 200M
+...
+```
+
+### workload.resource.maxStorage
+
+Defines the ephemeral storage size limit for a given workload. See official K8s [documentation](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#setting-requests-and-limits-for-local-ephemeral-storage).
+
+#### Default: `nil` (not set)
+
+#### Possible options: Arbitrary [Memory units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory). Examples: `64Mi`, `1Gi`...
+
+> workload.resource.maxStorage:
+```yaml
+version: 3.7
+services:
+  my-service:
+    x-k8s:
+      workload:
+        resource:
+          maxStorage: 10Gi
+...
+```
 
 ## workload.livenessProbe
 
@@ -483,7 +522,7 @@ Kev uses the following heuristics to derive that information for each service:
 If compose file(s) specifies the `healthcheck.test` attribute key in a service config it will use its value.
 If probe is not defined, it will prompt the user to define one by injecting a generic echo command.
 
-#### Default: echo "<generic prompt text>" 
+#### Default: echo "<generic prompt text>"
 
 #### Possible options: shell command
 
@@ -502,10 +541,10 @@ services:
 ...
 ```
 
-### workload.livenessProbe.http.port 
+### workload.livenessProbe.http.port
 
-Defines the liveness probe port to be used for the workload when the type is `http`. 
-See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request). 
+Defines the liveness probe port to be used for the workload when the type is `http`.
+See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request).
 
 #### Possible options: Integer
 
@@ -525,8 +564,8 @@ services:
 
 ### workload.livenessProbe.http.path
 
-Defines the liveness probe path to be used for the workload when the type is `http`. 
-See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request). 
+Defines the liveness probe path to be used for the workload when the type is `http`.
+See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request).
 
 #### Possible options: String
 
@@ -729,15 +768,15 @@ services:
         readinessProbe:
           type: exec
           exec:
-            command: 
+            command:
             - /is-my-service-ready.sh
 ...
 ```
 
 ### workload.readinessProbe.http.port
 
-Defines the readiness probe port to be used for the workload when the type is `http`. 
-See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes). 
+Defines the readiness probe port to be used for the workload when the type is `http`.
+See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes).
 
 #### Possible options: Integer
 
@@ -757,8 +796,8 @@ services:
 
 ### workload.readinessProbe.http.path
 
-Defines the readiness probe path to be used for the workload when the type is `http`. 
-See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes). 
+Defines the readiness probe path to be used for the workload when the type is `http`.
+See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes).
 
 #### Possible options: String
 
@@ -779,8 +818,8 @@ services:
 
 ### workload.readinessProbe.tcp.port
 
-Defines the readiness probe path to be used for the workload when the type is `tcp`. 
-See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes). 
+Defines the readiness probe path to be used for the workload when the type is `tcp`.
+See official K8s [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes).
 
 #### Possible options: Integer
 
@@ -1055,7 +1094,7 @@ services:
 
 ### service.expose.ingressAnnotations
 
-Ingress annotations are used to configure some options depending on the Ingress controller. Different Ingress controller support different annotations. See official K8s [documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource) 
+Ingress annotations are used to configure some options depending on the Ingress controller. Different Ingress controller support different annotations. See official K8s [documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource)
 
 NOTE: This option is only relevant when service is exposed, see: [service.expose.domain](#service.expose.domain) above.
 
diff --git a/pkg/kev/config/svcext.go b/pkg/kev/config/svcext.go
index abbb193..caf1888 100644
--- a/pkg/kev/config/svcext.go
+++ b/pkg/kev/config/svcext.go
@@ -429,10 +429,12 @@ type Workload struct {
 }
 
 type Resource struct {
-	Memory    string `yaml:"memory,omitempty"`
-	MaxMemory string `yaml:"maxMemory,omitempty"`
-	CPU       string `yaml:"cpu,omitempty"`
-	MaxCPU    string `yaml:"maxCpu,omitempty"`
+	Memory     string `yaml:"memory,omitempty"`
+	MaxMemory  string `yaml:"maxMemory,omitempty"`
+	CPU        string `yaml:"cpu,omitempty"`
+	MaxCPU     string `yaml:"maxCpu,omitempty"`
+	Storage    string `yaml:"storage,omitempty"`
+	MaxStorage string `yaml:"maxStorage,omitempty"`
 }
 
 type ImagePull struct {
diff --git a/pkg/kev/converter/kubernetes/project_service.go b/pkg/kev/converter/kubernetes/project_service.go
index c1558a0..99011d8 100644
--- a/pkg/kev/converter/kubernetes/project_service.go
+++ b/pkg/kev/converter/kubernetes/project_service.go
@@ -242,14 +242,18 @@ func (p *ProjectService) placement() map[string]string {
 }
 
 // resourceRequests returns workload resource requests (memory & cpu)
-// It parses CPU & Memory as k8s resource.Quantity regardless
+// It parses CPU, Memory & Ephemeral Storage as k8s resource.Quantity regardless
 // of how values are supplied (via deploy block or an extension).
+// Note: Only CPU & Memory requests can be set via docker compose deploy block!
+//       Storage can only be set via extension parameter.
 // It supports resource notations:
 // - CPU: 0.1, 100m (which is the same as 0.1), 1
 // - Memory: 1, 1M, 1m, 1G, 1Gi
-func (p *ProjectService) resourceRequests() (*int64, *int64) {
+// - Storage: 128974848, 10M, 100Mi, 1G, 2Gi
+func (p *ProjectService) resourceRequests() (*int64, *int64, *int64) {
 	var memRequest int64
 	var cpuRequest int64
+	var storageRequest int64
 
 	// @step extract requests from deploy block if present
 	if p.Deploy != nil && p.Deploy.Resources.Reservations != nil {
@@ -268,18 +272,27 @@ func (p *ProjectService) resourceRequests() (*int64, *int64) {
 		cpuRequest = v.ToDec().MilliValue()
 	}
 
-	return &memRequest, &cpuRequest
+	if val := p.SvcK8sConfig.Workload.Resource.Storage; val != "" {
+		v, _ := resource.ParseQuantity(val)
+		storageRequest, _ = v.AsInt64()
+	}
+
+	return &memRequest, &cpuRequest, &storageRequest
 }
 
 // resourceLimits returns workload resource limits (memory & cpu)
-// It parses CPU & Memory as k8s resource.Quantity regardless
+// It parses CPU, Memory & Ephemeral Storage as k8s resource.Quantity regardless
 // of how values are supplied (via deploy block or an extension).
+// Note: Only CPU & Memory requests can be set via docker compose deploy block!
+//       Storage can only be set via extension parameter.
 // It supports resource notations:
 // - CPU: 0.1, 100m (which is the same as 0.1), 1
 // - Memory: 1, 1M, 1m, 1G, 1Gi
-func (p *ProjectService) resourceLimits() (*int64, *int64) {
+// - Storage: 128974848, 10M, 100Mi, 1G, 2Gi
+func (p *ProjectService) resourceLimits() (*int64, *int64, *int64) {
 	var memLimit int64
 	var cpuLimit int64
+	var storageLimit int64
 
 	// @step extract limits from deploy block if present
 	if p.Deploy != nil && p.Deploy.Resources.Limits != nil {
@@ -297,7 +310,12 @@ func (p *ProjectService) resourceLimits() (*int64, *int64) {
 		cpuLimit = v.ToDec().MilliValue()
 	}
 
-	return &memLimit, &cpuLimit
+	if val := p.SvcK8sConfig.Workload.Resource.MaxStorage; val != "" {
+		v, _ := resource.ParseQuantity(val)
+		storageLimit, _ = v.AsInt64()
+	}
+
+	return &memLimit, &cpuLimit, &storageLimit
 }
 
 // runAsUser returns pod security context runAsUser value
diff --git a/pkg/kev/converter/kubernetes/transform.go b/pkg/kev/converter/kubernetes/transform.go
index 1c78dec..c8de5ae 100644
--- a/pkg/kev/converter/kubernetes/transform.go
+++ b/pkg/kev/converter/kubernetes/transform.go
@@ -1871,9 +1871,9 @@ func (k *Kubernetes) removeDupObjects(objs *[]runtime.Object) {
 // @orig: https://github.com/kubernetes/kompose/blob/master/pkg/transformer/kubernetes/k8sutils.go#L592
 func (k *Kubernetes) setPodResources(projectService ProjectService, template *v1.PodTemplateSpec) {
 	// @step resource limits
-	memLimit, cpuLimit := projectService.resourceLimits()
+	memLimit, cpuLimit, storageLimit := projectService.resourceLimits()
 
-	if *memLimit > 0 || *cpuLimit > 0 {
+	if *memLimit > 0 || *cpuLimit > 0 || *storageLimit > 0 {
 		resourceLimits := v1.ResourceList{}
 
 		if *memLimit > 0 {
@@ -1884,13 +1884,17 @@ func (k *Kubernetes) setPodResources(projectService ProjectService, template *v1
 			resourceLimits[v1.ResourceCPU] = *resource.NewMilliQuantity(*cpuLimit, resource.DecimalSI)
 		}
 
+		if *storageLimit > 0 {
+			resourceLimits[v1.ResourceEphemeralStorage] = *resource.NewQuantity(*storageLimit, resource.BinarySI)
+		}
+
 		template.Spec.Containers[0].Resources.Limits = resourceLimits
 	}
 
 	// @step resource requests
-	memRequest, cpuRequest := projectService.resourceRequests()
+	memRequest, cpuRequest, storageRequest := projectService.resourceRequests()
 
-	if *memRequest > 0 || *cpuRequest > 0 {
+	if *memRequest > 0 || *cpuRequest > 0 || *storageRequest > 0 {
 		resourceRequests := v1.ResourceList{}
 
 		if *memRequest > 0 {
@@ -1901,6 +1905,10 @@ func (k *Kubernetes) setPodResources(projectService ProjectService, template *v1
 			resourceRequests[v1.ResourceCPU] = *resource.NewMilliQuantity(*cpuRequest, resource.DecimalSI)
 		}
 
+		if *storageRequest > 0 {
+			resourceRequests[v1.ResourceEphemeralStorage] = *resource.NewQuantity(*storageRequest, resource.BinarySI)
+		}
+
 		template.Spec.Containers[0].Resources.Requests = resourceRequests
 	}
 }


Test patch:
diff --git a/pkg/kev/converter/kubernetes/project_service_test.go b/pkg/kev/converter/kubernetes/project_service_test.go
index d2ab2ac..59704b6 100644
--- a/pkg/kev/converter/kubernetes/project_service_test.go
+++ b/pkg/kev/converter/kubernetes/project_service_test.go
@@ -636,9 +636,10 @@ var _ = Describe("ProjectService", func() {
 		Context("not specified by deploy block", func() {
 			When("not specified via extension", func() {
 				It("returns resource request as zero values", func() {
-					mem, cpu := projectService.resourceRequests()
+					mem, cpu, storage := projectService.resourceRequests()
 					Expect(*mem).To(BeEquivalentTo(0))
 					Expect(*cpu).To(BeEquivalentTo(0))
+					Expect(*storage).To(BeEquivalentTo(0))
 				})
 			})
 		})
@@ -657,9 +658,10 @@ var _ = Describe("ProjectService", func() {
 
 			When("not specified via extension", func() {
 				It("returns resource request as defined in deploy block", func() {
-					mem, cpu := projectService.resourceRequests()
+					mem, cpu, storage := projectService.resourceRequests()
 					Expect(*mem).To(BeEquivalentTo(1000))
 					Expect(*cpu).To(BeEquivalentTo(100))
+					Expect(*storage).To(BeEquivalentTo(0))
 				})
 			})
 
@@ -669,7 +671,7 @@ var _ = Describe("ProjectService", func() {
 				})
 
 				It("returns CPU request as defined by the extension", func() {
-					_, cpu := projectService.resourceRequests()
+					_, cpu, _ := projectService.resourceRequests()
 					Expect(*cpu).To(BeEquivalentTo(200))
 				})
 			})
@@ -680,10 +682,21 @@ var _ = Describe("ProjectService", func() {
 				})
 
 				It("returns Memory request as defined by the extension", func() {
-					mem, _ := projectService.resourceRequests()
+					mem, _, _ := projectService.resourceRequests()
 					Expect(*mem).To(BeEquivalentTo(1000000))
 				})
 			})
+
+			When("Storage request is specified via extension", func() {
+				BeforeEach(func() {
+					svcK8sConfig.Workload.Resource.Storage = "0.5G"
+				})
+
+				It("returns Storage request as defined by the extension", func() {
+					_, _, storage := projectService.resourceRequests()
+					Expect(*storage).To(BeEquivalentTo(500000000))
+				})
+			})
 		})
 	})
 
@@ -691,9 +704,10 @@ var _ = Describe("ProjectService", func() {
 		Context("not specified by deploy block", func() {
 			When("not specified via extension", func() {
 				It("returns resource limits as zero values", func() {
-					mem, cpu := projectService.resourceLimits()
+					mem, cpu, storage := projectService.resourceLimits()
 					Expect(*mem).To(BeEquivalentTo(0))
 					Expect(*cpu).To(BeEquivalentTo(0))
+					Expect(*storage).To(BeEquivalentTo(0))
 				})
 			})
 		})
@@ -712,9 +726,10 @@ var _ = Describe("ProjectService", func() {
 
 			When("not specified via extension", func() {
 				It("returns resource limit as defined in deploy block", func() {
-					mem, cpu := projectService.resourceLimits()
+					mem, cpu, storage := projectService.resourceLimits()
 					Expect(*mem).To(BeEquivalentTo(1000))
 					Expect(*cpu).To(BeEquivalentTo(100))
+					Expect(*storage).To(BeEquivalentTo(0))
 				})
 			})
 
@@ -724,7 +739,7 @@ var _ = Describe("ProjectService", func() {
 				})
 
 				It("returns CPU limit as defined by the extension", func() {
-					_, cpu := projectService.resourceLimits()
+					_, cpu, _ := projectService.resourceLimits()
 					Expect(*cpu).To(BeEquivalentTo(200))
 				})
 			})
@@ -735,10 +750,21 @@ var _ = Describe("ProjectService", func() {
 				})
 
 				It("returns Memory limit as defined by the extension", func() {
-					mem, _ := projectService.resourceLimits()
+					mem, _, _ := projectService.resourceLimits()
 					Expect(*mem).To(BeEquivalentTo(200))
 				})
 			})
+
+			When("Ephemeral Storage limit is specified via extension", func() {
+				BeforeEach(func() {
+					svcK8sConfig.Workload.Resource.MaxStorage = "1G"
+				})
+
+				It("returns Ephemeral Storage limit as defined by the extension", func() {
+					_, _, storage := projectService.resourceLimits()
+					Expect(*storage).To(BeEquivalentTo(1000000000))
+				})
+			})
 		})
 	})