Repository: allegro/marathon-consul
Pull request #292 (2019-07-12 08:43:38)

Resolving port placeholders in Marathon labels
Adds a feature of port placeholders in Marathon labels that are resolved in consul tags when registering a service.

Placeholders format: `{port:<port name>}`, where `<port name>` should be a name of an existing port definitions. Such placeholder will be replaced by related port number when mapping Marathon labels into Consul tags.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24525809/badge)](https://coveralls.io/builds/24525809)

Coverage increased (+0.1%) to 90.718% when pulling **958f0a757195bbc1d788996156299365d7d9181e on pbetkier:port_placeholders_in_labels** into **149a59e4e26d44cbaa7d28c6792107a919d26a51 on allegro:master**.


Patch:
diff --git a/apps/app.go b/apps/app.go
index 369248d..ee67957 100644
--- a/apps/app.go
+++ b/apps/app.go
@@ -2,6 +2,7 @@ package apps
 
 import (
 	"encoding/json"
+	"fmt"
 	"strings"
 
 	log "github.com/sirupsen/logrus"
@@ -26,6 +27,7 @@ type HealthCheck struct {
 
 type PortDefinition struct {
 	Labels map[string]string `json:"labels"`
+	Name   string            `json:"name,omitempty"`
 }
 
 type Container struct {
@@ -99,7 +101,7 @@ func (app App) RegistrationIntentsNumber() int {
 		return 0
 	}
 
-	definitions := app.findConsulPortDefinitions()
+	definitions := app.filterConsulDefinitions(app.extractIndexedPortDefinitions())
 	if len(definitions) == 0 {
 		return 1
 	}
@@ -109,9 +111,11 @@ func (app App) RegistrationIntentsNumber() int {
 
 func (app App) RegistrationIntents(task *Task, nameSeparator string) []RegistrationIntent {
 	taskPortsCount := len(task.Ports)
-	definitions := app.findConsulPortDefinitions()
-	commonTags := labelsToTags(app.Labels)
-	if len(definitions) == 0 && taskPortsCount != 0 {
+	indexedPortDefinitions := app.extractIndexedPortDefinitions()
+	consulPortDefinitions := app.filterConsulDefinitions(indexedPortDefinitions)
+	tagPlaceholderMapping := createTagPlaceholderMapping(indexedPortDefinitions, task.Ports)
+	commonTags := labelsToTags(app.Labels, tagPlaceholderMapping)
+	if len(consulPortDefinitions) == 0 && taskPortsCount != 0 {
 		return []RegistrationIntent{
 			{
 				Name: app.labelsToName(app.Labels, nameSeparator),
@@ -122,7 +126,7 @@ func (app App) RegistrationIntents(task *Task, nameSeparator string) []Registrat
 	}
 
 	var intents []RegistrationIntent
-	for _, d := range definitions {
+	for _, d := range consulPortDefinitions {
 		if d.Index >= taskPortsCount {
 			log.WithField("Id", task.ID.String()).Warnf("Port index (%d) out of bounds should be from range [0,%d)", d.Index, taskPortsCount)
 			continue
@@ -130,7 +134,7 @@ func (app App) RegistrationIntents(task *Task, nameSeparator string) []Registrat
 		intents = append(intents, RegistrationIntent{
 			Name: app.labelsToName(d.Labels, nameSeparator),
 			Port: task.Ports[d.Index],
-			Tags: append(labelsToTags(d.Labels), commonTags...),
+			Tags: append(labelsToTags(d.Labels, tagPlaceholderMapping), commonTags...),
 		})
 	}
 	return intents
@@ -140,16 +144,35 @@ func marathonAppNameToServiceName(name string, nameSeparator string) string {
 	return strings.Replace(strings.Trim(strings.TrimSpace(name), "/"), "/", nameSeparator, -1)
 }
 
-func labelsToTags(labels map[string]string) []string {
+func labelsToTags(labels map[string]string, tagPlaceholderMapping map[string]string) []string {
 	tags := make([]string, 0, len(labels))
 	for key, value := range labels {
 		if value == "tag" {
-			tags = append(tags, key)
+			tags = append(tags, resolvePlaceholders(key, tagPlaceholderMapping))
 		}
 	}
 	return tags
 }
 
+func createTagPlaceholderMapping(portDefinitions []indexedPortDefinition, ports []int) map[string]string {
+	mapping := map[string]string{}
+	for _, d := range portDefinitions {
+		if d.Name != "" {
+			placeholder := fmt.Sprintf("{port:%s}", d.Name)
+			mapping[placeholder] = fmt.Sprint(ports[d.Index])
+		}
+	}
+	return mapping
+}
+
+func resolvePlaceholders(value string, tagPlaceholderMapping map[string]string) string {
+	for placeholder, replacement := range tagPlaceholderMapping {
+		value = strings.Replace(value, placeholder, replacement, -1)
+	}
+
+	return value
+}
+
 func (app App) labelsToName(labels map[string]string, nameSeparator string) string {
 	appConsulName := app.labelsToRawName(labels)
 	serviceName := marathonAppNameToServiceName(appConsulName, nameSeparator)
@@ -164,20 +187,31 @@ func (app App) labelsToName(labels map[string]string, nameSeparator string) stri
 type indexedPortDefinition struct {
 	Index  int
 	Labels map[string]string
+	Name   string
 }
 
-func (app App) findConsulPortDefinitions() []indexedPortDefinition {
+func (app App) extractIndexedPortDefinitions() []indexedPortDefinition {
 	var definitions []indexedPortDefinition
 	for i, d := range app.extractPortDefinitions() {
+		definitions = append(definitions, indexedPortDefinition{
+			Index:  i,
+			Labels: d.Labels,
+			Name:   d.Name,
+		})
+	}
+
+	return definitions
+}
+
+func (app App) filterConsulDefinitions(all []indexedPortDefinition) []indexedPortDefinition {
+	var consulDefinitions []indexedPortDefinition
+	for _, d := range all {
 		if _, ok := d.Labels[MarathonConsulLabel]; ok {
-			definitions = append(definitions, indexedPortDefinition{
-				Index:  i,
-				Labels: d.Labels,
-			})
+			consulDefinitions = append(consulDefinitions, d)
 		}
 	}
 
-	return definitions
+	return consulDefinitions
 }
 
 // Deprecated: Allows for backward compatibility with Marathons' network API


Test patch:
diff --git a/apps/app_test.go b/apps/app_test.go
index 1e89084..8de1f85 100644
--- a/apps/app_test.go
+++ b/apps/app_test.go
@@ -394,6 +394,66 @@ func TestRegistrationIntent_MultipleIntentsViaPortDefinitionIfMultipleContainCon
 	assert.Equal(t, []string{"second-tag", "common-tag"}, intents[1].Tags)
 }
 
+func TestRegistrationIntent_PortPlaceholderInPortDefinitionsLabel(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			{
+				Labels: map[string]string{"consul": "true", "something:{port:foo}": "tag"},
+			},
+			{
+				Name: "foo",
+			},
+		},
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intents := app.RegistrationIntents(task, "-")
+
+	// then
+	assert.Len(t, intents, 1)
+	assert.Equal(t, "app-name", intents[0].Name)
+	assert.Equal(t, 1234, intents[0].Port)
+	assert.Equal(t, []string{"something:5678"}, intents[0].Tags)
+}
+
+func TestRegistrationIntent_PortPlaceholderInPortDefinitionsLabel_NameNotFound(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			{
+				Labels: map[string]string{"consul": "true", "something:{port:bar}": "tag"},
+			},
+			{
+				Name: "foo",
+			},
+		},
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intents := app.RegistrationIntents(task, "-")
+
+	// then
+	assert.Len(t, intents, 1)
+	assert.Equal(t, "app-name", intents[0].Name)
+	assert.Equal(t, 1234, intents[0].Port)
+	assert.Equal(t, []string{"something:{port:bar}"}, intents[0].Tags)
+}
+
 func TestRegistrationIntent_TaskHasLessPortsThanApp(t *testing.T) {
 	t.Parallel()