gliderlabs / registrator

Service registry bridge for Docker with pluggable adapters
http://gliderlabs.com/registrator
MIT License
4.66k stars 912 forks source link

Add support for Go template to -tags flag #677

Open psyhomb opened 4 years ago

psyhomb commented 4 years ago

v7.4.0

About

I have added support for Go template to -tags flag and with this feature it is now possible to dynamically evaluate all the container inspect object fields and to add their values to Consul tags list, also prebuilt docker image is available on dockerhub.

Static tags can be used as before (non-breaking change):

-tags='test,service,backend' 

Or you can utilize Go template and dynamically evaluate all the fields from docker container inspect object:

-tags='container_id={{ .ID }}' 

Go Template functions

List of supported custom template functions (to check predefined global template functions click here):

strSlice

Slice string from start to end (same as s[start:end] where s represents string).

Usage: strSlice s start end

Example: strSlice .ID 0 12

{
    "Id": "e20f9c1a76565d62ae24a3bb877b17b862b6eab94f4e95a0e07ccf25087aaf4f"
}

Output: "e20f9c1a7656"


sIndex

Return element from slice or array s by specifiying index i (same as s[i] where s represents slice or array - index i can also take negative values to extract elements in reverse order).

Usage: sIndex i s

Example: sIndex 0 .Config.Env

{
    "Config": {
        "Env": [
            "ENVIRONMENT=test",
            "SERVICE_8105_NAME=foo",
            "HOME=/home/foobar",
            "SERVICE_9404_NAME=bar"
        ]
    }
}

Output: "ENVIRONMENT=test"


mIndex

Return value for key k stored in the map m (same as m["k"]).

Usage: mIndex k m

Example: mIndex "com.amazonaws.ecs.task-arn" .Config.Labels

{
    "Config": {
        "Labels": {
        "com.amazonaws.ecs.task-arn": "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf"
         }
    }
}

Output: "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf"


toUpper

Return string s with all letters mapped to their upper case.

Usage: toUpper s

Example: toUpper "foo" Output: FOO


toLower

Return string s with all letters mapped to their lower case.

Usage: toLower s

Example: toLower "FoO" Output: foo


replace

Replace all (-1) or first n occurrences of old with new found in the designated string s.

Usage: replace n old new s

Example: replace -1 "=" "" "=foo=" Output: foo


join

Create a single string from all the elements found in the slice s where sep will be used as separator.

Usage: join sep s

Example: join "," .Config.Env

{
    "Config": {
        "Env": [
            "ENVIRONMENT=test",
            "SERVICE_8105_NAME=foo",
            "HOME=/home/foobar",
            "SERVICE_9404_NAME=bar"
        ]
    }
}

Output: "ENVIRONMENT=test,SERVICE_8105_NAME=foo,HOME=/home/foobar,SERVICE_9404_NAME=bar"


split

Split string s into all substrings separated by sep and return a slice of the substrings between those separators.

Usage: split sep s

Example: split "," "/proc/bus,/proc/fs,/proc/irq" Output: [/proc/bus /proc/fs /proc/irq]


splitIndex

split and sIndex function combined, index i can also take negative values to extract elements in reverse order. Same result can be achieved if using pipeline with both functions: {{ split sep s | sIndex i }}

Usage: splitIndex i sep s

Example: splitIndex -1 "/" "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf" Output: "368f4403-0ee4-4f4c-b7a5-be50c57db5cf"


matchFirstElement

Iterate through slice s and return first element that match regex expression.

Usage: matchFirstElement regex s

Example: matchFirstElement "^SERVICE_" .Config.Env

{
    "Config": {
        "Env": [
            "ENVIRONMENT=test",
            "SERVICE_8105_NAME=foo",
            "HOME=/home/foobar",
            "SERVICE_9404_NAME=bar"
        ]
    }
}

Output: "SERVICE_8105_NAME=foo"


matchAllElements

Iterate through slice s and return slice of all elements that match regex expression.

Usage: matchAllElements regex s

Example: matchAllElements "^SERVICE_" .Config.Env

{
    "Config": {
        "Env": [
            "ENVIRONMENT=test",
            "SERVICE_8105_NAME=foo",
            "HOME=/home/foobar",
            "SERVICE_9404_NAME=bar"
        ]
    }
}

Output: [SERVICE_8105_NAME=foo SERVICE_9404_NAME=bar]


httpGet

Fetch an object from URL.

Usage: httpGet url

Example: httpGet "https://ajpi.me/all" Output: []byte (e.g. JSON object)


jsonParse

Extract value from JSON object by specifying exact path (nested objects). Keys in path has to be separated with double colon sign.

Usage: jsonParse b key1::key2::key3::keyN

Example: jsonParse b "Additional::Country"

{
    "Additional": {
        "Country": "United States"
    }
}

Output: "United States"


Examples

E1

Use custom (strSlice) or predefined global (slice) template function to slice values as needed:

-tags='container_id={{ strSlice .ID 0 12 }}' 

For multiple tags you'd just have to add comma separated values.

-tags='container_id={{ strSlice .ID 0 12 }},container_ip={{ .NetworkSettings.IPAddress }}'

Output:

container_id=37e7192110d2,container_ip=172.17.0.3

E2

In this example we're going to use mIndex template function to extract value from the Docker label that has dots in key name, then saved value will be passed to splitIndex template function which will be used to split string by / character and to return last element from the list.

Inspect container object snippet:

[
  {
    "Config": {
      "Labels": {
        "com.amazonaws.ecs.task-arn": "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf"
      }
    }
  }
]
-tags='ecs_task_id={{ mIndex "com.amazonaws.ecs.task-arn" .Config.Labels | splitIndex -1 "/" }}'

Output:

ecs_task_id=368f4403-0ee4-4f4c-b7a5-be50c57db5cf

E3

It is also possible to set all env vars as lower case tags.

-tags='{{ join "," .Config.Env | toLower }}'

Output:

env_name=test,service_name=foo,service_port=80

Or just one specific environment variable that match regex expression (first match only).

-tags='{{ matchFirstElement "^SERVICE_NAME=" .Config.Env | toLower }}'

Output:

service_name=foo

Or list of all regex matches.

-tags='{{ matchAllElements "^SERVICE_" .Config.Env | join "," | toLower }}'

Output:

service_name=foo,service_port=80

E4

Use httpGet function to fetch data from any external HTTP source (must return a JSON object).

-tags='{{ $httpBody := httpGet "https://ajpi.me/all" }}country={{ jsonParse $httpBody "Additional::Country" }}'

Output:

country=Australia

Or if running on AWS EC2 instance you can use this function and EC2 metadata API to fetch EC2 instanceId and instanceType.

-tags='{{ $httpBody := httpGet "http://169.254.169.254/latest/dynamic/instance-identity/document" }}ec2_instance_id={{ jsonParse $httpBody "instanceId" }},ec2_instance_type={{ jsonParse $httpBody "instanceType" }}'

Output:

ec2_instance_id=i-0a9f1423579a777a2,ec2_instance_type=t3.micro

Or you can use more advanced example that will allow you to use different sources and tags per container.

docker run -it -d \
  --name registrator \
  --network host \
  --restart always \
  --volume /var/run/docker.sock:/tmp/docker.sock \
  psyhomb/registrator:v7.4.0 -tags='
  {{- $s := matchFirstElement "^TAGS_URL=" .Config.Env | split "=" }}
  {{- if $url := slice $s 1 (len $s) | join "=" }}
    {{- if $httpBody := httpGet $url }}
      {{- range $i, $reg_tags_env := matchAllElements "^REG_TAGS_KEY_[0-9]+=" .Config.Env }}
        {{- $reg_tags_key := splitIndex 1 "=" $reg_tags_env }}
        {{- replace -1 "::" "_" $reg_tags_key | replace 1 "__" "=" | toLower }}{{ replace -1 "__" "" $reg_tags_key | jsonParse $httpBody | printf "%s," }}
      {{- end }}
    {{- end }}
  {{- end }}' \
  consul://127.0.0.1:8500

Now you can start one or more containers with special environment variables.

container 1

docker run -it -d -p 80 \
  -e SERVICE_NAME=foo \
  -e TAGS_URL="https://ajpi.me/all?ip=1.1.1.1" \
  -e REG_TAGS_KEY_1="Additional::Country__" \
  -e REG_TAGS_KEY_2="Additional::TimeZone__" \
  -e REG_TAGS_KEY_3="ClientIP__" \
  -e REG_TAGS_KEY_4="Hostname__" \
  -e REG_TAGS_KEY_5="Additional::Latitude__" \
  ubuntu:20.04

Output:

additional_latitude=-33.494
hostname=one.one.one.one.
clientip=1.1.1.1
additional_timezone=Australia/Sydney
additional_country=Australia

container 2

docker run -it -d -p 81 \
  -e SERVICE_NAME=bar \
  -e TAGS_URL="http://169.254.169.254/latest/dynamic/instance-identity/document" \
  -e REG_TAGS_KEY_1="instanceId__" \
  -e REG_TAGS_KEY_2="instanceType__" \
  ubuntu:20.04

Output:

instanceid=i-0a9f1423579a777a2
instancetype=t3.micro

Usage

To start using this feature you have to start registrator with -tags flag:

docker run -it -d \
  --name registrator \
  --network host \
  --restart always \
  --volume /var/run/docker.sock:/tmp/docker.sock \
  psyhomb/registrator:v7.4.0 -tags='container_id={{ strSlice .ID 0 12 }},container_ip={{ .NetworkSettings.IPAddress }}' consul://127.0.0.1:8500

Afterwards you can start any arbitrary container with SERVICE_NAME env variable and with one or multiple published ports:

docker run -it -d -e SERVICE_NAME=test -p 80 ubuntu:20.04

Check if container is started successfully:

docker ps -a -f "name=elated_bhabha"

Output:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                   NAMES
37e7192110d2        ubuntu:20.04        "/bin/bash"         10 minutes ago      Up 10 minutes       0.0.0.0:32795->80/tcp   elated_bhabha

Check registrator log:

docker logs -f registrator

Output:

2020/02/28 11:51:52 Starting registrator v7.4.0 ...
2020/02/28 11:51:52 Forcing host IP to 10.0.0.1
2020/02/28 11:51:52 Using consul adapter: consul://127.0.0.1:8500
2020/02/28 11:51:52 Connecting to backend (0/0)
2020/02/28 11:51:52 consul: current leader  10.0.0.10:8300
2020/02/28 11:51:52 Listening for Docker events ...
2020/02/28 11:51:52 Syncing services on 2 containers
2020/02/28 11:51:52 ignored: f01d4c20fd42 no published ports
2020/02/28 11:51:52 added: 37e7192110d2 host1:elated_bhabha:80

And finally check what's registered on Consul:

curl -sSL http://127.0.0.1:8500/v1/agent/service/host1:elated_bhabha:80 | jq .

Output:

{
  "ID": "host1:elated_bhabha:80",
  "Service": "test",
  "Tags": [
    "container_ip=172.17.0.3",
    "container_id=37e7192110d2"
  ],
  "Meta": null,
  "Port": 32795,
  "Address": "10.0.0.1",
  "Weights": {
    "Passing": 1,
    "Warning": 1
  },
  "EnableTagOverride": false,
  "ContentHash": "acdc264063dc0e31"
}
Screen Shot 2020-02-28 at 13 20 45

This is especially useful if you are using registrator to register Prometheus targets, because you can use these tags as labels.

issmirnov commented 3 years ago

@psyhomb Do you have a docker image built for your fork? This repo seems inactive, and I'm looking for something newer to pull from. This feature would be highly useful in our deployments as well.

psyhomb commented 3 years ago

Hey @issmirnov, yes docker image is available on dockerhub. 😉

Latest image version with features described above is v7.4.0: docker pull psyhomb/registrator:v7.4.0

This is an example of how we are running it in the production environment on AWS ECS nodes:

#!/bin/bash

IP=$(ip a s dev eth0 | awk '/inet [0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/ {print $2}' | awk -F'/' '{print $1}')

docker run -d \
  --name registrator \
  --network host \
  --restart always \
  -v /var/run/docker.sock:/tmp/docker.sock \
  psyhomb/registrator:v7.4.0 -deregister=always -cleanup=true -explicit=true -ip=${IP} -tags='
  {{- printf "%s" "cluster_name=" }}{{ mIndex "com.amazonaws.ecs.cluster" .Config.Labels | printf "%s," }}
  {{- printf "%s" "environment=" }}{{ matchFirstElement "^ENVIRONMENT=" .Config.Env | splitIndex 1 "=" | printf "%s," }}
  {{- printf "%s" "platform_unit_id=" }}{{ mIndex "com.amazonaws.ecs.task-arn" .Config.Labels | splitIndex -1 "/" | printf "%s," }}
  {{- printf "%s" "container_id=" }}{{ strSlice .ID 0 12 | printf "%s," }}
  {{- matchFirstElement "^SERVICE_NAME=" .Config.Env | toLower }}' \
  consul://127.0.0.1:8500

Example of tags list for registered Consul service:

cluster_name=c1
environment=prod
platform_unit_id=5f0390aab96a4fcaa881ed92685a924d
container_id=82c083191a42
service_name=foo