Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Go template to -tags flag #677

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

psyhomb
Copy link

@psyhomb psyhomb commented Feb 28, 2020

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
Copy link

@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
Copy link
Author

psyhomb commented Jan 21, 2021

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

@psyhomb psyhomb force-pushed the master branch 2 times, most recently from af47571 to 7940306 Compare September 22, 2021 12:18
- Add custom Go template functions
- Update signiture of existing Go template fuctions to support pipelines
- Migrate to Go modules for managing dependencies
- Update versions of builder and runtime docker images (Dockerfile)
- Update README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants