Repository: appvia/kev
Pull request #701 (2022-11-08 17:50:24)

Add ability to specify prefix for exposed service hostname
This PR adds the ability to specify hostname prefix for the exposed service. 

When specified it'll be prepended to the domain name.

Example: 
 
For prefix `hello.` and domain name `world.example.com` it'll result in ingress hostname set as `hello.world.example.com`.

Example config:

```yaml
version: 3.7
services:
  my-service:
    x-k8s:
      service:
        expose:
          domainPrefix: hello.
          domain: world.example.com
...
```


Patch:
diff --git a/docs/reference/config-example.md b/docs/reference/config-example.md
index 504ecd3..3b93eae 100644
--- a/docs/reference/config-example.md
+++ b/docs/reference/config-example.md
@@ -16,6 +16,7 @@ services:                                                # compose services sect
         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
         expose:                                          # K8s configuration to expose a service externally (by default services are never exposed)
+          domainPrefix:                                  # Default: "" (no prefix). If defined it'll prefix exposed domain name.
           domain:                                        # Default: "" (no ingress). Possible options: "" | "default" | domain.com,otherdomain.com (comma separated domain names). When with "default" or domain name(s) - it'll generate an ingress object and expose service externally.
           tlsSecret:                                     # Default: "" (no tls). Kubernetes secret name where certs will be loaded from.
       workload:                                          # K8s workload configuration (only required if values are overridden)
diff --git a/docs/reference/config-params.md b/docs/reference/config-params.md
index 83e2a2d..c569042 100644
--- a/docs/reference/config-params.md
+++ b/docs/reference/config-params.md
@@ -1133,6 +1133,27 @@ services:
 ...
 ```
 
+### service.expose.domainPrefix
+When specified the domain will be prefixed with the value of this attribute.
+Prefix will be prepended to the specified domain name.
+Example: `domainPrefix: "hello."` and `domain: world.my-awesome-service.com` will result in `hello.world.my-awesome-service.com`.
+
+#### Default: `""` - No domain prefix will be used!
+
+> service.expose.domain:
+```yaml
+version: 3.7
+services:
+  my-service:
+    x-k8s:
+      service:
+        type: LoadBalancer
+        expose:
+          domainPrefix: hello
+          domain: world.my-awesome-service.com
+...
+```
+
 ### service.expose.tlsSecret
 
 Defines whether to use TLS for the exposed service and which secret name contains certificates for the service. See the official K8s [documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls).
diff --git a/pkg/kev/config/svcext.go b/pkg/kev/config/svcext.go
index f974adc..960b40b 100644
--- a/pkg/kev/config/svcext.go
+++ b/pkg/kev/config/svcext.go
@@ -465,6 +465,7 @@ type Service struct {
 }
 
 type Expose struct {
+	DomainPrefix       string            `yaml:"domainPrefix,omitempty"`
 	Domain             string            `yaml:"domain,omitempty"`
 	TlsSecret          string            `yaml:"tlsSecret,omitempty"`
 	IngressAnnotations map[string]string `yaml:"ingressAnnotations,omitempty"`
diff --git a/pkg/kev/converter/kubernetes/project_service.go b/pkg/kev/converter/kubernetes/project_service.go
index 88a59c7..1b3ec04 100644
--- a/pkg/kev/converter/kubernetes/project_service.go
+++ b/pkg/kev/converter/kubernetes/project_service.go
@@ -170,6 +170,21 @@ func (p *ProjectService) exposeService() (string, error) {
 	return val, nil
 }
 
+// prefixedDomain prefixes specified domain name when service is exposed
+func (p *ProjectService) prefixedDomain() (string, error) {
+	prefix := strings.TrimSpace(p.SvcK8sConfig.Service.Expose.DomainPrefix)
+	domain, err := p.exposeService()
+	if err != nil {
+		return "", err
+	}
+
+	if prefix != "" {
+		return fmt.Sprintf("%s%s", prefix, domain), nil
+	}
+
+	return domain, nil
+}
+
 // tlsSecretName returns TLS secret name for exposed service (to be used in the ingress configuration)
 func (p *ProjectService) tlsSecretName() string {
 	return p.SvcK8sConfig.Service.Expose.TlsSecret
@@ -290,7 +305,8 @@ func (p *ProjectService) placement() map[string]string {
 // 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.
+// 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
@@ -329,7 +345,8 @@ func (p *ProjectService) resourceRequests() (*int64, *int64, *int64) {
 // 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.
+// 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
diff --git a/pkg/kev/converter/kubernetes/transform.go b/pkg/kev/converter/kubernetes/transform.go
index d4657a9..4b451ed 100644
--- a/pkg/kev/converter/kubernetes/transform.go
+++ b/pkg/kev/converter/kubernetes/transform.go
@@ -646,7 +646,7 @@ func (k *Kubernetes) initJob(projectService ProjectService, replicas int) *v1bat
 // initIngress initialises ingress object
 // @orig: https://github.com/kubernetes/kompose/blob/master/pkg/transformer/kubernetes/kubernetes.go#L446
 func (k *Kubernetes) initIngress(projectService ProjectService, port int32) *networkingv1.Ingress {
-	expose, _ := projectService.exposeService()
+	expose, _ := projectService.prefixedDomain()
 	if expose == "" {
 		return nil
 	}
@@ -1328,7 +1328,7 @@ func (k *Kubernetes) configPVCVolumeSource(name string, readonly bool) *v1.Volum
 
 // configEnvs returns a list of sorted kubernetes EnvVar objects mapping all project service environment variables
 // NOTE: compose-go library preloads all environment variables defined in env_files (if any), and appends
-// 		  them to the list of explicitly provided environment variables.
+// them to the list of explicitly provided environment variables.
 // @orig: https://github.com/kubernetes/kompose/blob/master/pkg/transformer/kubernetes/kubernetes.go#L961
 func (k *Kubernetes) configEnvs(projectService ProjectService) ([]v1.EnvVar, error) {
 	envs := EnvSort{}


Test patch:
diff --git a/pkg/kev/converter/kubernetes/project_service_test.go b/pkg/kev/converter/kubernetes/project_service_test.go
index 5a26c1b..b0ea79b 100644
--- a/pkg/kev/converter/kubernetes/project_service_test.go
+++ b/pkg/kev/converter/kubernetes/project_service_test.go
@@ -579,6 +579,52 @@ var _ = Describe("ProjectService", func() {
 
 	})
 
+	Describe("prefixedDomain", func() {
+
+		Context("when prefix specified via an extension", func() {
+			prefix := "myprefix."
+			expose := "domain.com"
+
+			BeforeEach(func() {
+				svcK8sConfig.Service.Expose.DomainPrefix = prefix
+				svcK8sConfig.Service.Expose.Domain = expose
+			})
+
+			It("will use the extension value", func() {
+				Expect(projectService.prefixedDomain()).To(Equal("myprefix.domain.com"))
+			})
+		})
+
+		Context("when not specified via an extension", func() {
+			BeforeEach(func() {
+				svcK8sConfig.Service.Expose.Domain = "domain.com"
+			})
+
+			It("will return empty string", func() {
+				Expect(projectService.prefixedDomain()).To(Equal("domain.com"))
+			})
+		})
+
+		Describe("validations", func() {
+
+			Context("when service hasn't been exposed via an extension but TLS secret was provided", func() {
+				BeforeEach(func() {
+					svcK8sConfig.Service.Expose.DomainPrefix = "myprefix-"
+					svcK8sConfig.Service.Expose.Domain = ""
+					svcK8sConfig.Service.Expose.TlsSecret = "my-tls-secret-name"
+				})
+
+				It("returns an error", func() {
+					_, err := projectService.prefixedDomain()
+					Expect(err).To(HaveOccurred())
+					Expect(err).To(MatchError("service can't have TLS secret name when it hasn't been exposed"))
+				})
+			})
+
+		})
+
+	})
+
 	Describe("tlsSecretName", func() {
 
 		Context("when specified via an extension", func() {
diff --git a/pkg/kev/converter/kubernetes/transform_test.go b/pkg/kev/converter/kubernetes/transform_test.go
index c2cd0e4..a0ff324 100644
--- a/pkg/kev/converter/kubernetes/transform_test.go
+++ b/pkg/kev/converter/kubernetes/transform_test.go
@@ -979,6 +979,19 @@ var _ = Describe("Transform", func() {
 			})
 		})
 
+		When("project service extension is exposing the k8s service using a domain name and prefix", func() {
+			BeforeEach(func() {
+				projectService.SvcK8sConfig.Service.Expose.DomainPrefix = "myprefix."
+				projectService.SvcK8sConfig.Service.Expose.Domain = "domain.name"
+			})
+
+			It("initialises Ingress with the correct host name", func() {
+				ingress := k.initIngress(projectService, port)
+				host := ingress.Spec.Rules[0].Host
+				Expect(host).To(Equal("myprefix.domain.name"))
+			})
+		})
+
 		When("project service extension exposing the k8s service using a domain with a path", func() {
 			domain := "domain.name"
 			path := "path"