├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── hooks └── build └── rootfs ├── docker-entrypoint.d └── env_secrets_expand.sh └── docker-entrypoint.sh /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | ARG version="0.1.0-dev" 4 | ARG build_date="unknown" 5 | ARG commit_hash="unknown" 6 | ARG vcs_url="unknown" 7 | ARG vcs_branch="unknown" 8 | 9 | ENV PORTAINER_ADDR=mssing-portainer-address \ 10 | PORTAINER_USER=missing-username \ 11 | PORTAINER_PASS=missing-password \ 12 | SSL_IGNORE_CERTIFICATION_CHECK=0 \ 13 | SLEEP_IF_WORKER=10 14 | 15 | LABEL org.label-schema.vendor="Softonic" \ 16 | org.label-schema.name="Portainer-Endpoint" \ 17 | org.label-schema.description="Autoregisters the swarm nodes in Portainer." \ 18 | org.label-schema.usage="/src/README.md" \ 19 | org.label-schema.url="https://github.com/softonic/portainer-autoregister/blob/master/README.md" \ 20 | org.label-schema.vcs-url=$vcs_url \ 21 | org.label-schema.vcs-branch=$vcs_branch \ 22 | org.label-schema.vcs-ref=$commit_hash \ 23 | org.label-schema.version=$version \ 24 | org.label-schema.schema-version="1.0" \ 25 | org.label-schema.docker.cmd.devel="" \ 26 | org.label-schema.docker.params="HOST_HOSTNAME=Host hostname,\ 27 | PORTAINER_ADDR=Portainer address,\ 28 | PORTAINER_USER=Username to login,\ 29 | PORTAINER_PASS=Password to login, \ 30 | SSL_IGNORE_CERTIFICATION_CHECK=Allow insecure connections to portainer when using SSL, \ 31 | SLEEP_IF_WORKER=Seconds to wait before register the node in Portainer in case it's a worker \ 32 | " \ 33 | org.label-schema.build-date=$build_date 34 | 35 | RUN apk add --no-cache socat jq curl 36 | 37 | ADD rootfs / 38 | 39 | ENTRYPOINT [ "/docker-entrypoint.sh" ] 40 | CMD [ "/usr/bin/socat", "-d", "-d", "TCP-L:2375,fork", "UNIX:/var/run/docker.sock" ] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Softonic International S.A. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | This project won't be maintained anymore, now Portainer has its own agent which is natively integrated. 3 | 4 | Image here: https://hub.docker.com/r/portainer/agent/ 5 | Docs available here: https://portainer.readthedocs.io/en/stable/agent.html 6 | 7 | # Portainer Endpoint 8 | 9 | [![](https://images.microbadger.com/badges/image/softonic/portainer-endpoint.svg)](https://microbadger.com/images/softonic/portainer-endpoint "Get your own image badge on microbadger.com") [![](https://images.microbadger.com/badges/version/softonic/portainer-endpoint.svg)](https://microbadger.com/images/softonic/portainer-endpoint "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/commit/softonic/portainer-endpoint.svg)](https://microbadger.com/images/softonic/portainer-endpoint "Get your own commit badge on microbadger.com") 10 | 11 | This image allows to auto register all the swarm nodes in a Portainer running in the same cluster and network. 12 | 13 | ## The goal 14 | 15 | Currently when running under `Docker Swarm mode` portainer has visibility of services and their tasks, but from the task you cannot get the logs/ssh session/etc. 16 | 17 | In the container tab you have only access to the containers running in the host where Portainer is connected. In case you want to get this functionality on containers running on other nodes of the cluster you need to add manually the connection details, which is not a valid solution in an elastic swarm cluster where nodes are added and removed continuously. 18 | 19 | This image allows you to make automatic this process. 20 | 21 | ## How to use 22 | 23 | You need to create a global service and pass some options and env vars. 24 | 25 | Thanks to the "global service" concept in Swarm mode we can run a service ensuring that each node is running a service task. In case the cluster adds more nodes Swarm is the responsible of launch a new task in the node, it's totally automatic. 26 | 27 | ### Options 28 | - Network: It's important to attach this service in a network where Portainer is attached. 29 | - Mounts 30 | - Hostname file: This allows to detect the name of the host where the service task is running. 31 | - Docker socket: This allows to expose the socket to Portainer. 32 | 33 | ### Environment Variables 34 | 35 | - `HOST_HOSTNAME`: (Optional) Just in case you want to change the mount point of the file that contains the name of the host 36 | - `PORTAINER_ADDR`: Name and port where Portainer is configured 37 | - `PORTAINER_USER`: Username used to login to Portainer 38 | - `PORTAINER_PASS`: Password used to login to Portainer 39 | - `SSL_IGNORE_CERTIFICATION_CHECK`: Activate it in case you don't want to validate the certificate in the Portainer service 40 | - `SLEEP_IF_WORKER`: Seconds to wait before register the node if it's a worker 41 | 42 | The `SLEEP_IF_WORKER` is useful to avoid that a worker is the first to register to Portainer, because it loads by default 43 | the first registered endpoint, if it's a worker you won't have the cluster overview in a first sight. 44 | 45 | ### Example of use as a container 46 | 47 | This allows to register just the current node. You need to be sure that the `portainer` network is attachable. 48 | 49 | ``` bash 50 | docker container run \ 51 | --name portainer-endpoint \ 52 | --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ 53 | --mount type=bind,source=/etc/hostname,target=/etc/host_hostname \ 54 | --network portainer \ 55 | --hostname="{{.Node.Hostname}} \ 56 | -e PORTAINER_ADDR=portainer:9000 \ 57 | -e PORTAINER_USER=admin \ 58 | -e PORTAINER_PASS=12341234 \ 59 | softonic/portainer-endpoint 60 | ``` 61 | 62 | ### Example of use as a global service 63 | 64 | This allows to register each node in Portainer. 65 | 66 | ``` bash 67 | docker service create --with-registry-auth \ 68 | --name portainer-endpoint \ 69 | --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ 70 | --mount type=bind,source=/etc/hostname,target=/etc/host_hostname \ 71 | --hostname="{{.Node.Hostname}} \ 72 | --network portainer \ 73 | --mode global \ 74 | -e PORTAINER_ADDR=portainer:9000 \ 75 | -e PORTAINER_USER=admin \ 76 | -e PORTAINER_PASS=12341234 \ 77 | softonic/portainer-endpoint 78 | ``` 79 | 80 | ### Use in a stack with Portainer 81 | 82 | You can use a Swarm Stack to deploy Portainer and the automatic registration feature. Just execute the stack file in a swarm cluster: 83 | 84 | ``` bash 85 | git clone git@github.com:softonic/portainer-endpoint.git 86 | cd portainer-endpoint 87 | export VOLUME_DRIVER=local 88 | export PORTAINER_PASS=12341234 89 | export PORTAINER_ENC_PASS=$(docker run --rm httpd:2.4-alpine htpasswd -nbB admin ${PORTAINER_PASS} | cut -d ":" -f 2) 90 | echo $PORTAINER_PASS | docker secret create portainer_password.v1 --label portainer - 91 | docker stack deploy --compose-file docker-compose.yml portainer 92 | ``` 93 | 94 | Once the stack is deployed and running you can go to the port `9000` on any of your cluster nodes to reach Portainer. 95 | You'll see all the nodes in your cluster are already registered. 96 | 97 | #### Requirements 98 | 99 | - It needs Docker >17.04 because of the version of the stack file 100 | - It needs Docker >17.10.0 for get rid of the host name mounted as a volume (`--mount type=bind,source=/etc/hostname,target=/etc/host_hostname`) and the `-e HOST_HOSTNAME=/etc/host_hostname` variable. 101 | - In this example I'm using a secret for the Portainer password in the endpoints 102 | - Its usage is optional, you can use the environment variable with the password in clear text (not recommended) 103 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | networks: 4 | portainer: 5 | attachable: true 6 | 7 | volumes: 8 | portainer: 9 | driver: ${VOLUME_DRIVER:-local} 10 | 11 | services: 12 | portainer: 13 | image: portainer/portainer:${PORTAINER_VERSION:-latest} 14 | networks: 15 | - portainer 16 | ports: 17 | - 9000:9000 18 | volumes: 19 | - portainer:/data 20 | - /var/run/docker.sock:/var/run/docker.sock 21 | command: [ 22 | "--admin-password", "${PORTAINER_ENC_PASS}" 23 | ] 24 | secrets: 25 | - source: "portainer_password.v1" 26 | target: "portainer_password" 27 | uid: "33" 28 | gid: "33" 29 | mode: 0400 30 | deploy: 31 | mode: replicated 32 | replicas: 1 33 | placement: 34 | constraints: 35 | - node.role == manager 36 | resources: 37 | limits: 38 | cpus: '0.20' 39 | memory: 30M 40 | reservations: 41 | cpus: '0.10' 42 | memory: 25M 43 | 44 | portainer-endpoint: 45 | image: softonic/portainer-endpoint:${PORTAINER_ENDPOINT_VERSION:-latest} 46 | networks: 47 | - portainer 48 | volumes: 49 | - /var/run/docker.sock:/var/run/docker.sock 50 | hostname: "{{ .Node.Hostname }}" 51 | environment: 52 | PORTAINER_ADDR: "portainer:9000" 53 | PORTAINER_USER: "admin" 54 | PORTAINER_PASS: "--DOCKER-SECRET:portainer_password--" 55 | secrets: 56 | - source: "portainer_password.v1" 57 | target: "portainer_password" 58 | uid: "33" 59 | gid: "33" 60 | mode: 0400 61 | deploy: 62 | mode: global 63 | resources: 64 | limits: 65 | cpus: '0.10' 66 | memory: 20M 67 | reservations: 68 | cpus: '0.05' 69 | memory: 10M 70 | 71 | secrets: 72 | portainer_password.v1: 73 | external: true 74 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # $IMAGE_NAME var is injected into the build so the tag is correct. 4 | 5 | echo "Build hook running" 6 | docker build \ 7 | --build-arg version=$(git describe --tags) \ 8 | --build-arg commit_hash=$(git rev-parse HEAD) \ 9 | --build-arg vcs_url=$(git config --get remote.origin.url) \ 10 | --build-arg vcs_branch=$(git rev-parse --abbrev-ref HEAD) \ 11 | --build-arg build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 12 | -t $IMAGE_NAME . 13 | -------------------------------------------------------------------------------- /rootfs/docker-entrypoint.d/env_secrets_expand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | : ${ENV_SECRETS_DIR:=/run/secrets} 4 | 5 | env_secret_debug() 6 | { 7 | if [ ! -z "$ENV_SECRETS_DEBUG" ]; then 8 | echo -e "\033[1m$@\033[0m" 9 | fi 10 | } 11 | 12 | # usage: env_secret_expand VAR 13 | # ie: env_secret_expand 'XYZ_DB_PASSWORD' 14 | # (will check for "$XYZ_DB_PASSWORD" variable value for a placeholder that defines the 15 | # name of the docker secret to use instead of the original value. For example: 16 | # XYZ_DB_PASSWORD={{DOCKER-SECRET:my-db.secret}} 17 | env_secret_expand() { 18 | var="$1" 19 | eval val=\$$var 20 | if secret_name=$(expr match "$val" "--DOCKER-SECRET:\([^}]\+\)--$"); then 21 | secret="${ENV_SECRETS_DIR}/${secret_name}" 22 | env_secret_debug "Secret file for $var: $secret" 23 | if [ -f "$secret" ]; then 24 | val=$(cat "${secret}") 25 | export "$var"="$val" 26 | env_secret_debug "Expanded variable: $var=$val" 27 | else 28 | env_secret_debug "Secret file does not exist! $secret" 29 | fi 30 | fi 31 | } 32 | 33 | env_secrets_expand() { 34 | for env_var in $(printenv | cut -f1 -d"=") 35 | do 36 | env_secret_expand $env_var 37 | done 38 | 39 | if [ ! -z "$ENV_SECRETS_DEBUG" ]; then 40 | echo -e "\n\033[1mExpanded environment variables\033[0m" 41 | printenv 42 | fi 43 | } 44 | 45 | env_secrets_expand 46 | -------------------------------------------------------------------------------- /rootfs/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Using curl instead of httpie for reduce the image size. 4 | 5 | echo 6 | for f in /docker-entrypoint.d/*; do 7 | case "$f" in 8 | *.sh) echo "$0: running $f"; . "$f" ;; 9 | *) echo "$0: ignoring $f" ;; 10 | esac 11 | echo 12 | done 13 | 14 | if [ -z ${HOST_HOSTNAME+x} ]; then 15 | echo "Environment variable 'HOST_HOSTNAME' not set, we'll use the container hostname instead" 16 | host_hostname=$(hostname) 17 | else 18 | host_hostname=$(cat ${HOST_HOSTNAME}) 19 | fi 20 | 21 | insecure_request='' 22 | if [[ -n ${SSL_IGNORE_CERTIFICATION_CHECK} ]]; then 23 | insecure_request=' -k' 24 | fi 25 | 26 | # Obtain NodeID from docker socket 27 | node_id=$(curl -s --unix-socket /var/run/docker.sock http:/v1.27/info | jq -r '.Swarm.NodeID') 28 | if [ -n "$node_id" ]; then 29 | is_manager=$(curl -s --unix-socket /var/run/docker.sock http:/v1.27/info | jq --arg NodeID "$node_id" -c '.Swarm.RemoteManagers[] | select(.NodeID | contains($NodeID))') 30 | if [[ -z "$is_manager" ]]; then 31 | echo "Sleeping ${SLEEP_IF_WORKER} seconds because the node is not a manager" 32 | sleep ${SLEEP_IF_WORKER} 33 | else 34 | echo "This node is manager, don't sleep!" 35 | fi 36 | fi 37 | 38 | # Get Portainer JWT 39 | #jwt=$(http POST "${PORTAINER_ADDR}/api/auth" Username="${PORTAINER_USER}" Password="${PORTAINER_PASS}" | jq -r .jwt) 40 | jwt=$(curl -sf${insecure_request} -X POST -H "Accept: application/json, */*" -H "Content-Type: application/json" --data "{\"Username\": \"${PORTAINER_USER}\", \"Password\": \"${PORTAINER_PASS}\"}" "${PORTAINER_ADDR}/api/auth" | jq -r .jwt) 41 | 42 | [ -z "$jwt" ] && echo "Can't connect or login with Portainer" && exit 1 43 | 44 | # Check if the host is already registered 45 | # registered_hosts=$(http --auth-type=jwt --auth="${jwt}" ${PORTAINER_ADDR}/api/endpoints | jq --arg HOST "$host_hostname" -c '.[] | select(.Name == $HOST) | .Id') 46 | registered_hosts=$(curl -sf${insecure_request} -X GET -H "Accept: application/json, */*" -H "Content-Type: application/json" -H "Authorization: Bearer ${jwt}" ${PORTAINER_ADDR}/api/endpoints | jq --arg HOST "$host_hostname" -c '.[] | select(.Name == $HOST) | .Id') 47 | for i in $registered_hosts 48 | do 49 | echo Deleting previous found host name with id $i 50 | # http --auth-type=jwt --auth="${jwt}" DELETE ${PORTAINER_ADDR}/api/endpoints/$i 51 | curl -sf${insecure_request} -X DELETE -H "Accept: application/json, */*" -H "Content-Type: application/json" -H "Authorization: Bearer ${jwt}" ${PORTAINER_ADDR}/api/endpoints/${i} 52 | done 53 | 54 | # Register current host 55 | # http --auth-type=jwt --auth="${jwt}" POST ${PORTAINER_ADDR}/api/endpoints Name="${host_hostname}-endpoint" URL="tcp://${HOSTNAME}:2375" 56 | curl -sf${insecure_request} -X POST -H "Accept: application/json, */*" -H "Content-Type: application/json" -H "Authorization: Bearer ${jwt}" --data "{\"Name\": \"${host_hostname}\", \"URL\": \"tcp://${HOSTNAME}:2375\"}" ${PORTAINER_ADDR}/api/endpoints 57 | 58 | exec "$@" 59 | 60 | --------------------------------------------------------------------------------