Repository: allegro/marathon-consul
Pull request #114 (2016-09-02 17:25:25)

Multiple registrations
This PR enabled registering multiple service instances for a given marathon app, e.g. with different ports, names or tags. These additional registration details can be provided in `portDefinitions` part of the marathon app configuration. Previous configuration method, via app-level `labels`, is still supported and that has not changed.

I'm going to commit changes in README on Monday ;)

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

Coverage increased (+0.6%) to 79.42% when pulling **29d60d8b66b8b2711817f6d6fe6a1312e649447d on multiple-registrations** into **411e1d1d6032bb85856ed0c820b384719d9cf807 on develop**.

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

Coverage increased (+1.2%) to 80.082% when pulling **c08e0354fd638e7d0630a11b1ad2ed6985c7889c on multiple-registrations** into **411e1d1d6032bb85856ed0c820b384719d9cf807 on develop**.

pbetkier: I've added support for explicit ports, reworked checking if two apps have same names, added table-driven tests. What do you think now?

I'm working on the README at the moment, going to push that soon.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/7749265/badge)](https://coveralls.io/builds/7749265)

Coverage increased (+1.2%) to 80.082% when pulling **a7010a9919bc4839b6100a0cdb81e15506168414 on multiple-registrations** into **411e1d1d6032bb85856ed0c820b384719d9cf807 on develop**.

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

Coverage increased (+1.2%) to 80.082% when pulling **552f948789bd6f8c007fda11b1569e25272676c9 on multiple-registrations** into **411e1d1d6032bb85856ed0c820b384719d9cf807 on develop**.

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

Coverage increased (+0.9%) to 79.781% when pulling **741d90effd071b20b30b88c204bdd66479249e05 on multiple-registrations** into **411e1d1d6032bb85856ed0c820b384719d9cf807 on develop**.

janisz: LGTM :+1: 

Patch:
diff --git a/README.md b/README.md
index e4408bf..9234bc4 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,8 @@ curl -X GET http://localhost:8500/v1/catalog/service/my-new-app
 - Every service registration contains an additional tag `marathon-task` specifying the Marathon task id related to this registration.
 - If there are multiple ports in use for the same app, note that only the first one will be registered by marathon-consul in Consul.
 
+If you need to register your task under multiple ports, refer to *Advanced usage* section below.
+
 ### Task healthchecks
 
 - At least one HTTP healthcheck should be defined for a task. The task is registered when Marathon marks it as alive.
@@ -215,14 +217,78 @@ Endpoint  | Description
 `/health` | healthcheck - returns `OK`
 `/events` | event sink - returns `OK` if all keys are set in an event, error message otherwise
 
-### Known limitations
+## Advanced usage
 
-The following section describes known limitations in `marathon-consul`.
+### Register under multiple ports
 
-* In Marathon when a deployment changing the application's service name (by changing its `labels`) is being stopped, it changes app's configuration anyway.
-  This means we loose the link between the app and the services registered with the old name in Consul.
-  Later on, if another deployment takes place, new services are registered with a new name, the old ones are not being deregistered though.
-  A scheduled sync is required to wipe them out.
+If you need to map your Marathon task into multiple service registrations in Consul, you can configure marathon-consul 
+via Marathon's `portDefinitions`:
+
+```
+  "id": "my-new-app",
+  "labels": {
+    "consul": "",
+    "common-tag": "tag"
+  },
+  "portDefinitions": [
+    {
+      "port": 0,
+      "protocol": "tcp",
+      "labels": {
+        "consul": "my-app-custom-name"
+      }
+    },
+    {
+      "port": 0,
+      "protocol": "tcp",
+      "labels": {
+        "consul": "my-app-other-name",
+        "specific-tag": "tag"
+      }
+    }
+  ]
+```
+
+This configuration would result in two service registrations:
+
+```
+curl -X GET http://localhost:8500/v1/catalog/service/my-app-custom-name
+...
+"ServiceName": "my-app-custom-name",
+"ServiceTags": [
+  "marathon",
+  "common-tag",
+  "marathon-task:my-new-app.6a95bb03-6ad3-11e6-beaf-080027a7aca0"
+],
+"ServicePort": 31292,
+...
+
+curl -X GET http://localhost:8500/v1/catalog/service/my-app-other-name
+...
+"ServiceName": "my-app-other-name",
+"ServiceTags": [
+  "marathon",
+  "common-tag",
+  "specific-tag",
+  "marathon-task:my-new-app.6a95bb03-6ad3-11e6-beaf-080027a7aca0"
+],
+"ServicePort": 31293,
+...
+``` 
+
+If any port definition contains the `consul` label, then advanced configuration mode is enabled. As a result, only the ports 
+containing this label are registered, under the name specified as the label's value – with empty value resolved to default name.
+Names don't have to be unique – you can have multiple registrations under the same name, but on different ports, 
+perhaps with different tags. Note that the `consul` label still needs to be present in the top-level application labels, even
+though its value won't have any effect.
+
+Tags configured in the top-level application labels will be added to all registrations. Tags configured in the port definition 
+labels will be added only to corresponding registrations.
+
+You can either provide explicit port numbers or provide 0, in which case randomly assigned ports will be used. Read more
+in [Marathon's documentation](https://mesosphere.github.io/marathon/docs/ports.html).
+
+All registrations share the same `marathon-task` tag.
 
 ## Migration to version 1.x.x
 
@@ -248,6 +314,15 @@ configured in your marathon-consul installation if you did customize it.
 
 If you have multiple datacenters you should migrate each one separately.
 
+## Known limitations
+
+The following section describes known limitations in `marathon-consul`.
+
+* In Marathon when a deployment changing the application's service name (by changing its `labels`) is being stopped, it changes app's configuration anyway.
+  This means we loose the link between the app and the services registered with the old name in Consul.
+  Later on, if another deployment takes place, new services are registered with a new name, the old ones are not being deregistered though.
+  A scheduled sync is required to wipe them out.
+
 ## License
 
 Marathon-consul is released under the Apache 2.0 license (see [LICENSE](LICENSE))
diff --git a/apps/app.go b/apps/app.go
index 9095165..34ba744 100644
--- a/apps/app.go
+++ b/apps/app.go
@@ -2,6 +2,9 @@ package apps
 
 import (
 	"encoding/json"
+	"strings"
+
+	log "github.com/Sirupsen/logrus"
 )
 
 // Only Marathon apps with this label will be registered in Consul
@@ -20,6 +23,11 @@ type HealthCheck struct {
 	}
 }
 
+type PortDefinition struct {
+	Port   int               `json:"port"`
+	Labels map[string]string `json:"labels"`
+}
+
 type AppWrapper struct {
 	App App `json:"app"`
 }
@@ -29,10 +37,11 @@ type AppsResponse struct {
 }
 
 type App struct {
-	Labels       map[string]string `json:"labels"`
-	HealthChecks []HealthCheck     `json:"healthChecks"`
-	ID           AppId             `json:"id"`
-	Tasks        []Task            `json:"tasks"`
+	Labels          map[string]string `json:"labels"`
+	HealthChecks    []HealthCheck     `json:"healthChecks"`
+	ID              AppId             `json:"id"`
+	Tasks           []Task            `json:"tasks"`
+	PortDefinitions []PortDefinition  `json:"portDefinitions"`
 }
 
 // Marathon Application Id (aka PathId)
@@ -49,8 +58,38 @@ func (app *App) IsConsulApp() bool {
 	return ok
 }
 
-func (app *App) ConsulName() string {
-	if value, ok := app.Labels[MARATHON_CONSUL_LABEL]; ok && !isSpecialConsulNameValue(value) {
+func (app *App) HasSameConsulNamesAs(other *App) bool {
+	thisNames := app.ConsulNames(".")
+	otherNames := other.ConsulNames(".")
+
+	if len(thisNames) != len(otherNames) {
+		return false
+	}
+
+	for i, name := range thisNames {
+		if name != otherNames[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func (app *App) ConsulNames(separator string) []string {
+	definitions := app.findConsulPortDefinitions()
+
+	if len(definitions) == 0 {
+		return []string{app.labelsToName(app.Labels, separator)}
+	}
+
+	var names []string
+	for _, d := range definitions {
+		names = append(names, app.labelsToName(d.Labels, separator))
+	}
+	return names
+}
+
+func (app *App) labelsToRawName(labels map[string]string) string {
+	if value, ok := labels[MARATHON_CONSUL_LABEL]; ok && !isSpecialConsulNameValue(value) {
 		return value
 	}
 	return app.ID.String()
@@ -73,3 +112,86 @@ func ParseApp(jsonBlob []byte) (*App, error) {
 
 	return &wrapper.App, err
 }
+
+type RegistrationIntent struct {
+	Name string
+	Port int
+	Tags []string
+}
+
+func (app *App) RegistrationIntents(task *Task, nameSeparator string) []*RegistrationIntent {
+	commonTags := labelsToTags(app.Labels)
+
+	definitions := app.findConsulPortDefinitions()
+	if len(definitions) == 0 {
+		return []*RegistrationIntent{
+			&RegistrationIntent{
+				Name: app.labelsToName(app.Labels, nameSeparator),
+				Port: task.Ports[0],
+				Tags: commonTags,
+			},
+		}
+	}
+
+	var intents []*RegistrationIntent
+	for _, d := range definitions {
+		intents = append(intents, &RegistrationIntent{
+			Name: app.labelsToName(d.Labels, nameSeparator),
+			Port: d.toPort(task),
+			Tags: append(commonTags, labelsToTags(d.Labels)...),
+		})
+	}
+	return intents
+}
+
+func marathonAppNameToServiceName(name string, nameSeparator string) string {
+	return strings.Replace(strings.Trim(strings.TrimSpace(name), "/"), "/", nameSeparator, -1)
+}
+
+func labelsToTags(labels map[string]string) []string {
+	tags := []string{}
+	for key, value := range labels {
+		if value == "tag" {
+			tags = append(tags, key)
+		}
+	}
+	return tags
+}
+
+func (app *App) labelsToName(labels map[string]string, nameSeparator string) string {
+	appConsulName := app.labelsToRawName(labels)
+	serviceName := marathonAppNameToServiceName(appConsulName, nameSeparator)
+	if serviceName == "" {
+		log.WithField("AppId", app.ID.String()).WithField("ConsulServiceName", appConsulName).
+		Warn("Warning! Invalid Consul service name provided for app. Will use default app name instead.")
+		return marathonAppNameToServiceName(app.ID.String(), nameSeparator)
+	}
+	return serviceName
+}
+
+type IndexedPortDefinition struct {
+	Index  int
+	Port   int
+	Labels map[string]string
+}
+
+func (i *IndexedPortDefinition) toPort(task *Task) int {
+	if i.Port == 0 {
+		return task.Ports[i.Index]
+	}
+	return i.Port
+}
+
+func (app *App) findConsulPortDefinitions() []IndexedPortDefinition {
+	var definitions []IndexedPortDefinition
+	for i, d := range app.PortDefinitions {
+		if _, ok := d.Labels[MARATHON_CONSUL_LABEL]; ok {
+			definitions = append(definitions, IndexedPortDefinition{
+				Index:  i,
+				Port:   d.Port,
+				Labels: d.Labels,
+			})
+		}
+	}
+	return definitions
+}
diff --git a/consul/consul.go b/consul/consul.go
index 14166c8..4f58df6 100644
--- a/consul/consul.go
+++ b/consul/consul.go
@@ -144,14 +144,14 @@ func contains(slice []string, search string) bool {
 }
 
 func (c *Consul) Register(task *apps.Task, app *apps.App) error {
-	service, err := c.marathonTaskToConsulService(task, app)
+	services, err := c.marathonTaskToConsulServices(task, app)
 	if err != nil {
 		return err
 	}
 	if value, ok := app.Labels[apps.MARATHON_CONSUL_LABEL]; ok && value == "true" {
 		log.WithField("Id", app.ID).Warn("Warning! Application configuration is deprecated (labeled as `consul:true`). Support for special `true` value will be removed in the future!")
 	}
-	metrics.Time("consul.register", func() { err = c.register(service) })
+	metrics.Time("consul.register", func() { err = c.registerMultipleServices(services) })
 	if err != nil {
 		metrics.Mark("consul.register.error")
 	} else {
@@ -160,6 +160,18 @@ func (c *Consul) Register(task *apps.Task, app *apps.App) error {
 	return err
 }
 
+func (c *Consul) registerMultipleServices(services []*consulapi.AgentServiceRegistration) error {
+	var registerErrors []error
+	for _, s := range services {
+		registerErr := c.register(s)
+		if registerErr != nil {
+			registerErrors = append(registerErrors, registerErr)
+		}
+	}
+
+	return utils.MergeErrorsOrNil(registerErrors, fmt.Sprintf("registering services"))
+}
+
 func (c *Consul) register(service *consulapi.AgentServiceRegistration) error {
 	agent, err := c.agents.GetAgent(service.Address)
 	if err != nil {
@@ -257,42 +269,32 @@ func (c *Consul) deregister(toDeregister *service.Service) error {
 	return err
 }
 
-func (c *Consul) ServiceName(app *apps.App) string {
-	appConsulName := app.ConsulName()
-	serviceName := c.marathonAppNameToConsulServiceName(appConsulName)
-	if serviceName == "" {
-		log.WithField("AppId", app.ID.String()).WithField("ConsulServiceName", appConsulName).
-			Warn("Warning! Invalid Consul service name provided for app. Will use default app name instead.")
-		return c.marathonAppNameToConsulServiceName(app.ID.String())
-	}
-	return serviceName
-}
-
-func (c *Consul) marathonAppNameToConsulServiceName(name string) string {
-	return strings.Replace(strings.Trim(strings.TrimSpace(name), "/"), "/", c.config.ConsulNameSeparator, -1)
+func (c *Consul) ServiceNames(app *apps.App) []string {
+	return app.ConsulNames(c.config.ConsulNameSeparator)
 }
 
-func (c *Consul) marathonTaskToConsulService(task *apps.Task, app *apps.App) (*consulapi.AgentServiceRegistration, error) {
+func (c *Consul) marathonTaskToConsulServices(task *apps.Task, app *apps.App) ([]*consulapi.AgentServiceRegistration, error) {
 	IP, err := utils.HostToIPv4(task.Host)
 	if err != nil {
 		return nil, err
 	}
 	serviceAddress := IP.String()
-
-	name := c.ServiceName(app)
-	port := task.Ports[0]
-	serviceID := c.serviceId(task, name, port)
-	tags := c.marathonLabelsToConsulTags(app.Labels)
-	tags = append(tags, service.MarathonTaskTag(task.ID))
-
-	return &consulapi.AgentServiceRegistration{
-		ID:      serviceID,
-		Name:    name,
-		Port:    port,
-		Address: serviceAddress,
-		Tags:    tags,
-		Checks:  c.marathonToConsulChecks(task, app.HealthChecks, serviceAddress),
-	}, nil
+	checks := c.marathonToConsulChecks(task, app.HealthChecks, serviceAddress)
+
+	var registrations []*consulapi.AgentServiceRegistration
+	for _, intent := range app.RegistrationIntents(task, c.config.ConsulNameSeparator) {
+		tags := append([]string{c.config.Tag}, intent.Tags...)
+		tags = append(tags, service.MarathonTaskTag(task.ID))
+		registrations = append(registrations, &consulapi.AgentServiceRegistration{
+			ID:      c.serviceId(task, intent.Name, intent.Port),
+			Name:    intent.Name,
+			Port:    intent.Port,
+			Address: serviceAddress,
+			Tags:    tags,
+			Checks:  checks,
+		})
+	}
+	return registrations, nil
 }
 
 func (c *Consul) serviceId(task *apps.Task, name string, port int) string {
@@ -340,16 +342,6 @@ func (c *Consul) marathonToConsulChecks(task *apps.Task, healthChecks []apps.Hea
 	return checks
 }
 
-func (c *Consul) marathonLabelsToConsulTags(labels map[string]string) []string {
-	tags := []string{c.config.Tag}
-	for key, value := range labels {
-		if value == "tag" {
-			tags = append(tags, key)
-		}
-	}
-	return tags
-}
-
 func (c *Consul) AddAgentsFromApps(apps []*apps.App) {
 	for _, app := range apps {
 		if !app.IsConsulApp() {
diff --git a/consul/consul_stub.go b/consul/consul_stub.go
index da7103b..99b245e 100644
--- a/consul/consul_stub.go
+++ b/consul/consul_stub.go
@@ -84,29 +84,33 @@ func (c *ConsulStub) Register(task *apps.Task, app *apps.App) error {
 	if _, ok := c.failRegisterForIds[task.ID]; ok {
 		return fmt.Errorf("Consul stub programmed to fail when registering task of id %s", task.ID.String())
 	} else {
-		serviceRegistration, err := c.consul.marathonTaskToConsulService(task, app)
+		serviceRegistrations, err := c.consul.marathonTaskToConsulServices(task, app)
 		if err != nil {
 			return err
 		}
-		c.services[service.ServiceId(serviceRegistration.ID)] = serviceRegistration
+		for _, r := range serviceRegistrations {
+			c.services[service.ServiceId(r.ID)] = r
+		}
 		return nil
 	}
 }
 
 func (c *ConsulStub) RegisterWithoutMarathonTaskTag(task *apps.Task, app *apps.App) {
-	serviceRegistration := consulapi.AgentServiceRegistration{
-		ID:      task.ID.String(),
-		Name:    app.ConsulName(),
-		Port:    task.Ports[0],
-		Address: "127.0.0.1",
-		Tags:    []string{c.consul.config.Tag},
-		Checks:  consulapi.AgentServiceChecks{},
+	for _, intent := range app.RegistrationIntents(task, c.consul.config.ConsulNameSeparator) {
+		serviceRegistration := consulapi.AgentServiceRegistration{
+			ID:      task.ID.String(),
+			Name:    intent.Name,
+			Port:    intent.Port,
+			Address: task.Host,
+			Tags:    intent.Tags,
+			Checks:  consulapi.AgentServiceChecks{},
+		}
+		c.services[service.ServiceId(serviceRegistration.ID)] = &serviceRegistration
 	}
-	c.services[service.ServiceId(serviceRegistration.ID)] = &serviceRegistration
 }
 
-func (c *ConsulStub) ServiceName(app *apps.App) string {
-	return c.consul.ServiceName(app)
+func (c *ConsulStub) ServiceNames(app *apps.App) []string {
+	return c.consul.ServiceNames(app)
 }
 
 func (c *ConsulStub) DeregisterByTask(taskId apps.TaskId) error {
diff --git a/events/deployment_info.go b/events/deployment_info.go
index 4253735..c57a649 100644
--- a/events/deployment_info.go
+++ b/events/deployment_info.go
@@ -92,7 +92,7 @@ func (d *DeploymentEvent) RenamedConsulApps() []*apps.App {
 		targetMap := d.appsMap(target)
 		for id, originalApp := range originalMap {
 			targetApp, ok := targetMap[id]
-			if !ok || originalApp.ConsulName() != targetApp.ConsulName() {
+			if !ok || !originalApp.HasSameConsulNamesAs(targetApp) {
 				renamedApps = append(renamedApps, originalApp)
 			}
 		}
diff --git a/service/service.go b/service/service.go
index 9e06f31..09bb81b 100644
--- a/service/service.go
+++ b/service/service.go
@@ -40,5 +40,5 @@ type ServiceRegistry interface {
 	Register(task *apps.Task, app *apps.App) error
 	DeregisterByTask(taskId apps.TaskId) error
 	Deregister(toDeregister *Service) error
-	ServiceName(app *apps.App) string
+	ServiceNames(app *apps.App) []string
 }
diff --git a/web/event_handler.go b/web/event_handler.go
index ecbdacf..74e9665 100644
--- a/web/event_handler.go
+++ b/web/event_handler.go
@@ -218,26 +218,28 @@ func (fh *eventHandler) handleDeploymentStepSuccess(body []byte) error {
 }
 
 func (fh *eventHandler) deregisterAllAppServices(app *apps.App) []error {
-
 	errors := []error{}
-	serviceName := fh.serviceRegistry.ServiceName(app)
-
-	log.WithField("AppId", app.ID).WithField("ServiceName", serviceName).Info("Deregistering all services")
+	serviceNames := fh.serviceRegistry.ServiceNames(app)
 
-	services, err := fh.serviceRegistry.GetServices(serviceName)
+	log.WithField("AppId", app.ID).WithField("ServiceNames", serviceNames).Info("Deregistering all services")
 
-	if err != nil {
-		log.WithField("Id", app.ID).WithError(err).Error("There was a problem getting Consul services")
-		errors = append(errors, err)
-		return errors
+	var allServices []*service.Service
+	for _, name := range serviceNames {
+		services, err := fh.serviceRegistry.GetServices(name)
+		if err != nil {
+			log.WithField("Id", app.ID).WithError(err).Error("There was a problem getting Consul services")
+			errors = append(errors, err)
+			return errors
+		}
+		allServices = append(allServices, services...)
 	}
 
-	if len(services) == 0 {
-		log.WithField("AppId", app.ID).WithField("ServiceName", serviceName).Info("No matching Consul services found")
+	if len(allServices) == 0 {
+		log.WithField("AppId", app.ID).WithField("ServiceNames", serviceNames).Info("No matching Consul services found")
 		return errors
 	}
 
-	for _, service := range services {
+	for _, service := range allServices {
 		taskId, err := service.TaskId()
 		if err != nil {
 			errors = append(errors, err)


Test patch:
diff --git a/apps/app_test.go b/apps/app_test.go
index aafb856..060031c 100644
--- a/apps/app_test.go
+++ b/apps/app_test.go
@@ -119,3 +119,411 @@ func TestAppId_String(t *testing.T) {
 	t.Parallel()
 	assert.Equal(t, "appId", AppId("appId").String())
 }
+
+var dummyTask = &Task{
+	ID:    TaskId("some-task"),
+	Ports: []int{1337},
+}
+
+func TestRegistrationIntent_NameWithoutConsulLabel(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID: "/rootGroup/subGroup/subSubGroup/name",
+	}
+
+	// when
+	intent := app.RegistrationIntents(dummyTask, ".")[0]
+
+	// then
+	assert.Equal(t, "rootGroup.subGroup.subSubGroup.name", intent.Name)
+}
+
+func TestRegistrationIntent_Name(t *testing.T) {
+	t.Parallel()
+
+	var intentNameTestsData = []struct {
+		consulLabel  string
+		expectedName string
+	}{
+		{"", "rootGroup-subGroup-subSubGroup-name"},
+		{"true", "rootGroup-subGroup-subSubGroup-name"},
+		{"/some-other/name", "some-other-name"},
+		{"     ///", "rootGroup-subGroup-subSubGroup-name"},
+	}
+
+	for _, testData := range intentNameTestsData {
+		// given
+		app := &App{
+			ID:     "/rootGroup/subGroup/subSubGroup/name",
+			Labels: map[string]string{"consul": testData.consulLabel},
+		}
+
+		// when
+		intent := app.RegistrationIntents(dummyTask, "-")[0]
+
+		// then
+		if intent.Name != testData.expectedName {
+			t.Errorf("Registration name from consul label '%s' was '%s', expected '%s'", testData.consulLabel, intent.Name, testData.expectedName)
+		}
+	}
+}
+
+func TestRegistrationIntent_PickFirstPort(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID: "name",
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intent := app.RegistrationIntents(task, "-")[0]
+
+	// then
+	assert.Equal(t, 1234, intent.Port)
+}
+
+func TestRegistrationIntent_WithTags(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "name",
+		Labels: map[string]string{"private": "tag", "other": "irrelevant"},
+	}
+
+	// when
+	intent := app.RegistrationIntents(dummyTask, "-")[0]
+
+	// then
+	assert.Equal(t, []string{"private"}, intent.Tags)
+}
+
+func TestRegistrationIntent_NoOverrideViaPortDefinitionsIfNoConsulLabelThere(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true", "private": "tag"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"other": "tag"},
+			},
+			PortDefinition{},
+		},
+	}
+	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{"private"}, intents[0].Tags)
+}
+
+func TestRegistrationIntent_OverrideNameAndAddTagsViaPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true", "private": "tag"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "other-name", "other": "tag"},
+			},
+			PortDefinition{},
+		},
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intents := app.RegistrationIntents(task, "-")
+
+	// then
+	assert.Len(t, intents, 1)
+	assert.Equal(t, "other-name", intents[0].Name)
+	assert.Equal(t, 1234, intents[0].Port)
+	assert.Equal(t, []string{"private", "other"}, intents[0].Tags)
+}
+
+func TestRegistrationIntent_PickDifferentPortViaPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true", "private": "tag"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{},
+			PortDefinition{
+				Labels: map[string]string{"consul": "true"},
+			},
+		},
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intent := app.RegistrationIntents(task, "-")[0]
+
+	// then
+	assert.Equal(t, 5678, intent.Port)
+}
+
+func TestRegistrationIntent_PickExplicitPortViaPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true", "private": "tag"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Port:   1337,
+				Labels: map[string]string{"consul": "true"},
+			},
+		},
+	}
+	task := &Task{
+		Ports: []int{},
+	}
+
+	// when
+	intent := app.RegistrationIntents(task, "-")[0]
+
+	// then
+	assert.Equal(t, 1337, intent.Port)
+}
+
+func TestRegistrationIntent_MultipleIntentsViaPortDefinitionIfMultipleContainConsulLabel(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true", "common-tag": "tag"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "first-name", "first-tag": "tag"},
+			},
+			PortDefinition{
+				Labels: map[string]string{"consul": "second-name", "second-tag": "tag"},
+			},
+		},
+	}
+	task := &Task{
+		Ports: []int{1234, 5678},
+	}
+
+	// when
+	intents := app.RegistrationIntents(task, "-")
+
+	// then
+	assert.Len(t, intents, 2)
+	assert.Equal(t, "first-name", intents[0].Name)
+	assert.Equal(t, 1234, intents[0].Port)
+	assert.Equal(t, []string{"common-tag", "first-tag"}, intents[0].Tags)
+	assert.Equal(t, "second-name", intents[1].Name)
+	assert.Equal(t, 5678, intents[1].Port)
+	assert.Equal(t, []string{"common-tag", "second-tag"}, intents[1].Tags)
+}
+
+func TestConsulNames_WithoutPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "context/app-name",
+		Labels: map[string]string{"consul": ""},
+	}
+
+	// expect
+	assert.Equal(t, []string{"context.app-name"}, app.ConsulNames("."))
+}
+
+func TestConsulNames_WithPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "context/app-name",
+		Labels: map[string]string{"consul": ""},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "some/name"},
+			},
+			PortDefinition{
+				Labels: map[string]string{"consul": "yet-another/name"},
+			},
+		},
+	}
+
+	// expect
+	assert.Equal(t, []string{"some.name", "yet-another.name"}, app.ConsulNames("."))
+}
+
+func TestHasSameConsulNamesAs_SameConfigsWithoutPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+	}
+
+	// expect
+	assert.True(t, app.HasSameConsulNamesAs(app))
+}
+
+func TestHasSameConsulNamesAs_DifferentConfigsSameNameWithoutPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+	}
+	other := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "app-name"},
+	}
+
+	// expect
+	assert.True(t, app.HasSameConsulNamesAs(other))
+	assert.True(t, other.HasSameConsulNamesAs(other))
+}
+
+func TestHasSameConsulNamesAs_DifferentConfigsWithoutPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "some-name"},
+	}
+	other := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "different-name"},
+	}
+
+	// expect
+	assert.False(t, app.HasSameConsulNamesAs(other))
+}
+
+func TestHasSameConsulNamesAs_SameConfigsWithPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "true"},
+			},
+		},
+	}
+
+	// expect
+	assert.True(t, app.HasSameConsulNamesAs(app))
+}
+
+func TestHasSameConsulNamesAs_DifferentConfigsSameNamesWithPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "true"},
+			},
+		},
+	}
+	other := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "app-name"},
+			},
+		},
+	}
+
+	// expect
+	assert.True(t, app.HasSameConsulNamesAs(other))
+	assert.True(t, other.HasSameConsulNamesAs(app))
+}
+
+func TestHasSameConsulNamesAs_DifferentConfigsDifferentNamesWithPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "some-name"},
+			},
+		},
+	}
+	other := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "other-name"},
+			},
+		},
+	}
+
+	// expect
+	assert.False(t, app.HasSameConsulNamesAs(other))
+}
+
+func TestHasSameConsulNamesAs_DifferentConfigsDifferentNumberOfRegitrationsWithPortDefinitions(t *testing.T) {
+	t.Parallel()
+
+	// given
+	app := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "some-name"},
+			},
+		},
+	}
+	other := &App{
+		ID:     "app-name",
+		Labels: map[string]string{"consul": "true"},
+		PortDefinitions: []PortDefinition{
+			PortDefinition{
+				Labels: map[string]string{"consul": "some-name"},
+			},
+			PortDefinition{
+				Labels: map[string]string{"consul": "yet-another-name"},
+			},
+		},
+	}
+
+	// expect
+	assert.False(t, app.HasSameConsulNamesAs(other))
+}
diff --git a/consul/consul_test.go b/consul/consul_test.go
index 254997f..a79b13a 100644
--- a/consul/consul_test.go
+++ b/consul/consul_test.go
@@ -385,6 +385,60 @@ func TestRegisterServices_CustomServiceName(t *testing.T) {
 	assert.Equal(t, "myCustomServiceName", services[0].Name)
 }
 
+func TestRegisterServices_MultipleRegistrations(t *testing.T) {
+	t.Parallel()
+	server := CreateConsulTestServer(t)
+	defer server.Stop()
+
+	consul := ConsulClientAtServer(server)
+	consul.config.Tag = "marathon"
+
+	// given
+	app := utils.ConsulApp("serviceA", 1)
+	app.PortDefinitions = []apps.PortDefinition{
+		apps.PortDefinition{
+			Labels: map[string]string{"consul": "first-name", "first-tag": "tag"},
+		},
+		apps.PortDefinition{
+			Labels: map[string]string{"consul": "second-name", "second-tag": "tag"},
+		},
+	}
+	app.Tasks[0].Host = server.Config.Bind
+	app.Tasks[0].Ports = []int{8080, 8081}
+	app.Labels["common-tag"] = "tag"
+
+	// when
+	err := consul.Register(&app.Tasks[0], app)
+
+	// then
+	assert.NoError(t, err)
+
+	// when
+	services, _ := consul.GetAllServices()
+
+	// then
+	assert.Len(t, services, 2)
+
+	first, found := findServiceByName("first-name", services)
+	assert.True(t, found, "first-name not found in services")
+	second, found := findServiceByName("second-name", services)
+	assert.True(t, found, "second-name not found in services")
+
+	assert.Equal(t, "first-name", first.Name)
+	assert.Equal(t, []string{"marathon", "common-tag", "first-tag", "marathon-task:serviceA.0"}, first.Tags)
+	assert.Equal(t, "second-name", second.Name)
+	assert.Equal(t, []string{"marathon", "common-tag", "second-tag", "marathon-task:serviceA.0"}, second.Tags)
+}
+
+func findServiceByName(name string, services []*service.Service) (*service.Service, bool) {
+	for _, s := range services {
+		if s.Name == name {
+			return s, true
+		}
+	}
+	return nil, false
+}
+
 func TestRegisterServices_InvalidHostnameShouldFail(t *testing.T) {
 	t.Parallel()
 	server := CreateConsulTestServer(t)
@@ -687,7 +741,8 @@ func TestMarathonTaskToConsulServiceMapping(t *testing.T) {
 	}
 
 	// when
-	service, err := consul.marathonTaskToConsulService(task, app)
+	services, err := consul.marathonTaskToConsulServices(task, app)
+	service := services[0]
 
 	// then
 	assert.NoError(t, err)
@@ -751,92 +806,8 @@ func TestMarathonTaskToConsulServiceMapping_NotResolvableTaskHost(t *testing.T)
 	}
 
 	// when
-	_, err := consul.marathonTaskToConsulService(task, app)
+	_, err := consul.marathonTaskToConsulServices(task, app)
 
 	// then
 	assert.Error(t, err)
 }
-
-func TestServiceName_WithSeparator(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app := &apps.App{
-		ID: "/rootGroup/subGroup/subSubGroup/name",
-	}
-	consul := New(ConsulConfig{ConsulNameSeparator: "."})
-
-	// when
-	serviceName := consul.ServiceName(app)
-
-	// then
-	assert.Equal(t, "rootGroup.subGroup.subSubGroup.name", serviceName)
-}
-
-func TestServiceName_WithEmptyConsulLabel(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app := &apps.App{
-		ID:     "/rootGroup/subGroup/subSubGroup/name",
-		Labels: map[string]string{"consul": ""},
-	}
-	consul := New(ConsulConfig{ConsulNameSeparator: "-"})
-
-	// when
-	serviceName := consul.ServiceName(app)
-
-	// then
-	assert.Equal(t, "rootGroup-subGroup-subSubGroup-name", serviceName)
-}
-
-func TestServiceName_WithConsulLabelSetToTrue(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app := &apps.App{
-		ID:     "/rootGroup/subGroup/subSubGroup/name",
-		Labels: map[string]string{"consul": "true"},
-	}
-	consul := New(ConsulConfig{ConsulNameSeparator: "-"})
-
-	// when
-	serviceName := consul.ServiceName(app)
-
-	// then
-	assert.Equal(t, "rootGroup-subGroup-subSubGroup-name", serviceName)
-}
-
-func TestServiceName_WithCustomConsulLabelEscapingChars(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app := &apps.App{
-		ID:     "/rootGroup/subGroup/subSubGroup/name",
-		Labels: map[string]string{"consul": "/some-other/name"},
-	}
-	consul := New(ConsulConfig{ConsulNameSeparator: "-"})
-
-	// when
-	serviceName := consul.ServiceName(app)
-
-	// then
-	assert.Equal(t, "some-other-name", serviceName)
-}
-
-func TestServiceName_WithInvalidLabelValue(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app := &apps.App{
-		ID:     "/rootGroup/subGroup/subSubGroup/name",
-		Labels: map[string]string{"consul": "     ///"},
-	}
-	consul := New(ConsulConfig{ConsulNameSeparator: "-"})
-
-	// when
-	serviceName := consul.ServiceName(app)
-
-	// then
-	assert.Equal(t, "rootGroup-subGroup-subSubGroup-name", serviceName)
-}
diff --git a/sync/error_service_registry_stub_test.go b/sync/error_service_registry_stub_test.go
index f1e738c..174a471 100644
--- a/sync/error_service_registry_stub_test.go
+++ b/sync/error_service_registry_stub_test.go
@@ -30,6 +30,6 @@ func (c errorServiceRegistry) Deregister(toDeregister *service.Service) error {
 	return fmt.Errorf("Error occured")
 }
 
-func (c errorServiceRegistry) ServiceName(app *apps.App) string {
-	return ""
+func (c errorServiceRegistry) ServiceNames(app *apps.App) []string {
+	return []string{}
 }
diff --git a/sync/sync_test.go b/sync/sync_test.go
index 3c87cdb..9bd0e58 100644
--- a/sync/sync_test.go
+++ b/sync/sync_test.go
@@ -160,14 +160,14 @@ func (c *ConsulServicesMock) RegistrationsCount(instanceId string) int {
 	return c.registrations[instanceId]
 }
 
-func (c *ConsulServicesMock) ServiceName(app *apps.App) string {
-	return ""
-}
-
 func (c *ConsulServicesMock) DeregisterByTask(taskId apps.TaskId) error {
 	return nil
 }
 
+func (c *ConsulServicesMock) ServiceNames(app *apps.App) []string {
+	return []string{}
+}
+
 func (c *ConsulServicesMock) Deregister(toDeregister *service.Service) error {
 	return nil
 }
diff --git a/web/event_handler_test.go b/web/event_handler_test.go
index 7f9de97..8e9ab9a 100644
--- a/web/event_handler_test.go
+++ b/web/event_handler_test.go
@@ -143,6 +143,7 @@ func TestWebHandler_HandleDeploymentInfoWithStopApplicationAction(t *testing.T)
 	service := newConsulStubWithApplicationsTasksRegistered(app)
 	assert.Len(t, service.RegisteredTaskIds(), 3)
 	handle, stop := NewHandler(Config{WorkersCount: 1}, marathon, service)
+	app.Tasks = []apps.Task{} // tasks are not available in this event
 	body, _ := json.Marshal(deploymentInfoWithStopApplicationActionForApps(app))
 	req, _ := http.NewRequest("POST", "/events", bytes.NewBuffer([]byte(body)))
 
@@ -232,31 +233,6 @@ func TestWebHandler_HandleDeploymentInfoWithStopApplicationActionForMultipleApps
 	assert.False(t, marathon.Interactions())
 }
 
-func TestWebHandler_HandleDeploymentInfoWithStopApplicationActionForMultipleAppsAndProblemsGettingServicesForOne(t *testing.T) {
-	t.Parallel()
-
-	// given
-	app1 := ConsulApp("/test/app", 3)
-	app2 := ConsulApp("/test/otherapp", 2)
-	marathon := marathon.MarathonerStubForApps()
-	service := newConsulStubWithApplicationsTasksRegistered(app1, app2)
-	service.FailGetServicesForName("test.app")
-	assert.Len(t, service.RegisteredTaskIds(), 5)
-	handle, stop := NewHandler(Config{WorkersCount: 1}, marathon, service)
-	body, _ := json.Marshal(deploymentInfoWithStopApplicationActionForApps(app1, app2))
-	req, _ := http.NewRequest("POST", "/events", bytes.NewBuffer([]byte(body)))
-
-	// when
-	recorder := httptest.NewRecorder()
-	handle(recorder, req)
-	stop()
-
-	// then
-	assertAccepted(t, recorder)
-	assert.Len(t, service.RegisteredTaskIds(), 3)
-	assert.False(t, marathon.Interactions())
-}
-
 func TestWebHandler_HandleDeploymentInfoWithStopApplicationActionWithNoServicesRegistered(t *testing.T) {
 	t.Parallel()