Compare commits

..

No commits in common. "master" and "jw-prs" have entirely different histories.

57 changed files with 2491 additions and 4388 deletions

View File

@ -1,9 +1,4 @@
.git
.github
docker-gen
dist
examples
Makefile
README.md
templates
*.gz

View File

@ -1,25 +0,0 @@
version: 2
updates:
# Maintain dependencies for Go modules
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "build"
# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "build"
# Maintain GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"

View File

@ -1,33 +0,0 @@
name: Release assets
on:
push:
tags:
- "*.*.*"
jobs:
assets:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Build release assets
run: make release
- name: Upload release assets
uses: alexellis/upload-assets@0.4.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
asset_paths: '["./docker-gen-*.tar.gz"]'
- name: Cleanup release assets
run: make dist-clean

View File

@ -1,87 +0,0 @@
name: Build and publish Docker images
on:
workflow_dispatch:
push:
tags:
- "*.*.*"
paths:
- ".dockerignore"
- ".github/workflows/build-publish.yml"
- "Dockerfile"
- "go.mod"
- "go.sum"
- "**.go"
jobs:
multiarch-build:
name: Build and publish image
strategy:
matrix:
base: [alpine]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Retrieve version
id: docker-gen_version
run: echo "VERSION=$(git describe --tags)" >> "$GITHUB_OUTPUT"
- name: Get Docker tags
id: docker_meta
uses: docker/metadata-action@v4
with:
images: |
btcpayserver/docker-gen
tags: |
type=semver,pattern={{version}},enable=${{ matrix.base == 'alpine' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.base == 'alpine' }}
type=semver,suffix=-debian,pattern={{version}},enable=${{ matrix.base == 'debian' }}
type=semver,suffix=-debian,pattern={{major}}.{{minor}},enable=${{ matrix.base == 'debian' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' && matrix.base == 'alpine' }}
type=raw,value=debian,enable=${{ github.ref == 'refs/heads/main' && matrix.base == 'debian' }}
labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ steps.docker-gen_version.outputs.VERSION }}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the image
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
build-args: DOCKER_GEN_VERSION=${{ steps.docker-gen_version.outputs.VERSION }}
platforms: linux/amd64,linux/arm/v7,linux/arm64
file: Dockerfile.${{ matrix.base }}
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Docker image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@ -1,38 +0,0 @@
name: Tests
on:
push:
branches:
- main
pull_request:
paths-ignore:
- "LICENSE"
- "**.md"
- "examples/*"
- "templates/*"
jobs:
unit:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Install dependencies
run: make get-deps
- name: Build docker-gen
run: make docker-gen
- name: Check code formatting
run: make check-gofmt
- name: Run tests
run: go test -race -v ./internal/...

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
docker-gen
!cmd/docker-gen
dist
*.gz

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: go
go:
- 1.4
install:
- make get-deps
script:
- make all check-gofmt test

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM debian:wheezy
MAINTAINER Jason Wilder <jason@influxdb.com>
ENV VERSION 0.4.3
ENV DOWNLOAD_URL https://github.com/jwilder/docker-gen/releases/download/$VERSION/docker-gen-linux-amd64-$VERSION.tar.gz
ENV DOCKER_HOST unix:///tmp/docker.sock
RUN deps=' \
curl ca-certificates \
'; \
set -x; \
apt-get update \
&& apt-get install -y --no-install-recommends $deps \
&& curl -L $DOWNLOAD_URL | tar -C /usr/local/bin -xvz \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false $deps \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/local/bin/docker-gen"]

View File

@ -1,31 +0,0 @@
ARG DOCKER_GEN_VERSION=main
# Build docker-gen from scratch
FROM golang:1.20.12-alpine as go-builder
ARG DOCKER_GEN_VERSION
WORKDIR /build
# Install the dependencies
COPY . .
RUN go mod download
# Build the docker-gen executable
RUN GOOS=linux CGO_ENABLED=0 go build -ldflags "-X main.buildVersion=${DOCKER_GEN_VERSION}" -o docker-gen ./cmd/docker-gen
FROM alpine:3.18.5
ARG DOCKER_GEN_VERSION
ENV DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Install packages required by the image
RUN apk add --no-cache --virtual .bin-deps openssl
# Install docker-gen from build stage
COPY --from=go-builder /build/docker-gen /usr/local/bin/docker-gen
# Copy the license
COPY LICENSE /usr/local/share/doc/docker-gen/
ENTRYPOINT ["/usr/local/bin/docker-gen"]

View File

@ -1,35 +0,0 @@
ARG DOCKER_GEN_VERSION=main
# Build docker-gen from scratch
FROM golang:1.20.12 as go-builder
ARG DOCKER_GEN_VERSION
WORKDIR /build
# Install the dependencies
COPY . .
RUN go mod download
# Build the docker-gen executable
RUN GOOS=linux CGO_ENABLED=0 go build -ldflags "-X main.buildVersion=${DOCKER_GEN_VERSION}" -o docker-gen ./cmd/docker-gen
FROM debian:12.2-slim
ARG VERSION
ENV DOCKER_GEN_VERSION=${VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Install packages required by the image
RUN apt-get update \
&& apt-get install -y -q --no-install-recommends openssl \
&& apt-get clean \
&& rm -r /var/lib/apt/lists/*
# Install docker-gen from build stage
COPY --from=go-builder /build/docker-gen /usr/local/bin/docker-gen
# Copy the license
COPY LICENSE /usr/local/share/doc/docker-gen/
ENTRYPOINT ["/usr/local/bin/docker-gen"]

2
GLOCKFILE Normal file
View File

@ -0,0 +1,2 @@
github.com/BurntSushi/toml 056c9bc7be7190eaa7715723883caffa5f8fa3e4
github.com/fsouza/go-dockerclient e0d22d30691bcc996eca51f729a4777b8c7dc2a8

View File

@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2021 Jason Wilder
Copyright (c) 2021-2022 Nicolas Duchon
Copyright (c) 2014 Jason Wilder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -17,43 +17,35 @@ dist-clean:
rm -f docker-gen-darwin-*.tar.gz
dist: dist-clean
mkdir -p dist/alpine-linux/amd64 && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/amd64/docker-gen ./cmd/docker-gen
mkdir -p dist/alpine-linux/arm64 && GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/arm64/docker-gen ./cmd/docker-gen
mkdir -p dist/alpine-linux/armhf && GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/armhf/docker-gen ./cmd/docker-gen
mkdir -p dist/alpine-linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/amd64/docker-gen ./cmd/docker-gen
mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/linux/amd64/docker-gen ./cmd/docker-gen
mkdir -p dist/linux/arm64 && GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o dist/linux/arm64/docker-gen ./cmd/docker-gen
mkdir -p dist/linux/i386 && GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o dist/linux/i386/docker-gen ./cmd/docker-gen
mkdir -p dist/linux/armel && GOOS=linux GOARCH=arm GOARM=5 go build -ldflags "$(LDFLAGS)" -o dist/linux/armel/docker-gen ./cmd/docker-gen
mkdir -p dist/linux/armhf && GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o dist/linux/armhf/docker-gen ./cmd/docker-gen
mkdir -p dist/darwin/amd64 && GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/darwin/amd64/docker-gen ./cmd/docker-gen
mkdir -p dist/darwin/i386 && GOOS=darwin GOARCH=386 go build -ldflags "$(LDFLAGS)" -o dist/darwin/i386/docker-gen ./cmd/docker-gen
release: dist
go mod tidy
glock sync -n < GLOCKFILE
tar -cvzf docker-gen-alpine-linux-amd64-$(TAG).tar.gz -C dist/alpine-linux/amd64 docker-gen
tar -cvzf docker-gen-alpine-linux-arm64-$(TAG).tar.gz -C dist/alpine-linux/arm64 docker-gen
tar -cvzf docker-gen-alpine-linux-armhf-$(TAG).tar.gz -C dist/alpine-linux/armhf docker-gen
tar -cvzf docker-gen-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 docker-gen
tar -cvzf docker-gen-linux-arm64-$(TAG).tar.gz -C dist/linux/arm64 docker-gen
tar -cvzf docker-gen-linux-i386-$(TAG).tar.gz -C dist/linux/i386 docker-gen
tar -cvzf docker-gen-linux-armel-$(TAG).tar.gz -C dist/linux/armel docker-gen
tar -cvzf docker-gen-linux-armhf-$(TAG).tar.gz -C dist/linux/armhf docker-gen
tar -cvzf docker-gen-darwin-amd64-$(TAG).tar.gz -C dist/darwin/amd64 docker-gen
tar -cvzf docker-gen-darwin-i386-$(TAG).tar.gz -C dist/darwin/i386 docker-gen
get-deps:
go mod download
go get github.com/robfig/glock
glock sync -n < GLOCKFILE
check-gofmt:
if [ -n "$(shell go fmt ./cmd/...)" ]; then \
if [ -n "$(shell gofmt -l .)" ]; then \
echo 1>&2 'The following files need to be formatted:'; \
gofmt -l ./cmd/docker-gen; \
exit 1; \
fi
if [ -n "$(shell go fmt ./internal/...)" ]; then \
echo 1>&2 'The following files need to be formatted:'; \
gofmt -l ./internal; \
gofmt -l .; \
exit 1; \
fi
test:
go test -race ./internal/...
go test

142
README.md
View File

@ -1,20 +1,17 @@
docker-gen
=====
[![Tests](https://github.com/nginx-proxy/docker-gen/actions/workflows/tests.yml/badge.svg)](https://github.com/nginx-proxy/docker-gen/actions/workflows/tests.yml)
[![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/docker-gen)](https://github.com/nginx-proxy/docker-gen/releases)
[![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/docker-gen?sort=semver)](https://hub.docker.com/r/nginxproxy/docker-gen "Click to view the image on Docker Hub")
[![Docker stars](https://img.shields.io/docker/stars/nginxproxy/docker-gen.svg)](https://hub.docker.com/r/nginxproxy/docker-gen 'DockerHub')
[![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/docker-gen.svg)](https://hub.docker.com/r/nginxproxy/docker-gen 'DockerHub')
![latest 0.4.3](https://img.shields.io/badge/latest-0.4.3-green.svg?style=flat)
[![Build Status](https://travis-ci.org/jwilder/docker-gen.svg?branch=master)](https://travis-ci.org/jwilder/docker-gen)
![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)
`docker-gen` is a file generator that renders templates using docker container meta-data.
It can be used to generate various kinds of files for:
* **Centralized logging** - [fluentd](https://github.com/nginx-proxy/docker-gen/blob/main/templates/fluentd.conf.tmpl), logstash or other centralized logging tools that tail the containers JSON log file or files within the container.
* **Log Rotation** - [logrotate](https://github.com/nginx-proxy/docker-gen/blob/main/templates/logrotate.tmpl) files to rotate container JSON log files
* **Reverse Proxy Configs** - [nginx](https://github.com/nginx-proxy/docker-gen/blob/main/templates/nginx.tmpl), [haproxy](https://github.com/jwilder/docker-discover), etc. reverse proxy configs to route requests from the host to containers
* **Centralized logging** - [fluentd](https://github.com/jwilder/docker-gen/blob/master/templates/fluentd.conf.tmpl), logstash or other centralized logging tools that tail the containers JSON log file or files within the container.
* **Log Rotation** - [logrotate](https://github.com/jwilder/docker-gen/blob/master/templates/logrotate.tmpl) files to rotate container JSON log files
* **Reverse Proxy Configs** - [nginx](https://github.com/jwilder/docker-gen/blob/master/templates/nginx.tmpl), [haproxy](https://github.com/jwilder/docker-discover), etc. reverse proxy configs to route requests from the host to containers
* **Service Discovery** - Scripts (python, bash, etc..) to register containers within [etcd](https://github.com/jwilder/docker-register), hipache, etc..
===
@ -28,17 +25,16 @@ There are three common ways to run docker-gen:
#### Host Install
Linux/OSX binaries for release [0.9.0](https://github.com/nginx-proxy/docker-gen/releases)
Linux/OSX binaries for release [0.4.3](https://github.com/jwilder/docker-gen/releases)
* [amd64](https://github.com/nginx-proxy/docker-gen/releases/download/0.9.0/docker-gen-linux-amd64-0.9.0.tar.gz)
* [i386](https://github.com/nginx-proxy/docker-gen/releases/download/0.9.0/docker-gen-linux-i386-0.9.0.tar.gz)
* [alpine-linux](https://github.com/nginx-proxy/docker-gen/releases/download/0.9.0/docker-gen-alpine-linux-amd64-0.9.0.tar.gz)
* [amd64](https://github.com/jwilder/docker-gen/releases/download/0.4.3/docker-gen-linux-amd64-0.4.3.tar.gz)
* [i386](https://github.com/jwilder/docker-gen/releases/download/0.4.3/docker-gen-linux-i386-0.4.3.tar.gz)
Download the version you need, untar, and install to your PATH.
```
$ wget https://github.com/nginx-proxy/docker-gen/releases/download/0.9.0/docker-gen-linux-amd64-0.9.0.tar.gz
$ tar xvzf docker-gen-linux-amd64-0.9.0.tar.gz
$ wget https://github.com/jwilder/docker-gen/releases/download/0.4.3/docker-gen-linux-amd64-0.4.3.tar.gz
$ tar xvzf docker-gen-linux-amd64-0.4.3.tar.gz
$ ./docker-gen
```
@ -46,19 +42,19 @@ $ ./docker-gen
Docker-gen can be bundled inside of a container along-side applications.
[nginx-proxy/nginx-proxy](https://hub.docker.com/r/nginxproxy/nginx-proxy) trusted build is an example of
[jwilder/nginx-proxy](https://index.docker.io/u/jwilder/nginx-proxy/) trusted build is an example of
running docker-gen within a container along-side nginx.
[jwilder/docker-register](https://github.com/jwilder/docker-register) is an example of running
docker-gen within a container to do service registration with etcd.
#### Separate Container Install
It can also be run as two separate containers using the [nginx-proxy/docker-gen](https://hub.docker.com/r/nginxproxy/docker-gen)
image, together with virtually any other image.
It can also be run as two separate containers using the [jwilder/docker-gen](https://index.docker.io/u/jwilder/docker-gen/)
image virtually any other image.
This is how you could run the official [nginx](https://registry.hub.docker.com/_/nginx/) image and
have docker-gen generate a reverse proxy config in the same way that `nginx-proxy` works. You may want to do
this to prevent having the docker socket bound to a publicly exposed container service.
have dockgen-gen generate a reverse proxy config in the same way that `nginx-proxy` works. You may want to do
this to prevent having the docker socket bound to an publicly exposed container service.
Start nginx with a shared volume:
@ -69,17 +65,11 @@ $ docker run -d -p 80:80 --name nginx -v /tmp/nginx:/etc/nginx/conf.d -t nginx
Fetch the template and start the docker-gen container with the shared volume:
```
$ mkdir -p /tmp/templates && cd /tmp/templates
$ curl -o nginx.tmpl https://raw.githubusercontent.com/nginx-proxy/docker-gen/main/templates/nginx.tmpl
$ curl -o nginx.tmpl https://raw.githubusercontent.com/jwilder/docker-gen/master/templates/nginx.tmpl
$ docker run -d --name nginx-gen --volumes-from nginx \
-v /var/run/docker.sock:/tmp/docker.sock:rw \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
-v /tmp/templates:/etc/docker-gen/templates \
-t nginxproxy/docker-gen -notify-sighup nginx -watch -only-exposed /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
```
Start a container, taking note of any Environment variables a container expects. See the top of a template for details.
```
$ docker run --env VIRTUAL_HOST='example.com' --env VIRTUAL_PORT=80 ...
-t jwilder/docker-gen -notify-sighup nginx -watch -only-exposed /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
```
===
@ -104,19 +94,12 @@ Options:
run command after template is regenerated (e.g restart xyz)
-notify-output
log the output(stdout/stderr) of notify command
-notify-container container-ID
container to send a signal to
-notify-signal signal
signal to send to the -notify-container. -1 to call docker restart. Defaults to 1 aka. HUP.
All available signals available on the [dockerclient](https://github.com/fsouza/go-dockerclient/blob/01804dec8a84d0a77e63611f2b62d33e9bb2b64a/signal.go)
-notify-sighup container-ID
send HUP signal to container. Equivalent to 'docker kill -s HUP container-ID', or `-notify-container container-ID -notify-signal 1`
-notify-sighup docker kill -s HUP container-ID
send HUP signal to container. Equivalent to docker kill -s HUP container-ID
-only-exposed
only include containers with exposed ports
-only-published
only include containers with published ports (implies -only-exposed)
-include-stopped
include stopped containers
-tlscacert string
path to TLS CA certificate file (default "/Users/jason/.docker/machine/machines/default/ca.pem")
-tlscert string
@ -129,12 +112,10 @@ Options:
show version
-watch
watch for container changes
-wait
minimum (and/or maximum) duration to wait after each container change before triggering
Arguments:
template - path to a template to generate
dest - path to write the template. If not specfied, STDOUT is used
dest - path to a write the template. If not specfied, STDOUT is used
Environment Variables:
DOCKER_HOST - default value for -endpoint
@ -142,12 +123,12 @@ Environment Variables:
DOCKER_TLS_VERIFY - enable client TLS verification]
```
If no `<dest>` file is specified, the output is sent to stdout. Mainly useful for debugging.
If no `<dest>` file is specified, the output is sent to stdout. Mainly useful for debugging.
### Configuration file
Using the -config flag from above you can tell docker-gen to use the specified config file instead of command-line options. Multiple templates can be defined and they will be executed in the order that they appear in the config file.
Using the -config flag from above you can tell docker-gen to use the specified config file instead of command-line options. Multiple templates can be defined and they will be executed in the order that they appear in the config file.
An example configuration file, **docker-gen.cfg** can be found in the examples folder.
@ -157,7 +138,7 @@ An example configuration file, **docker-gen.cfg** can be found in the examples f
Starts a configuration section
dest = "path/to/a/file"
path to write the template. If not specfied, STDOUT is used
path to a write the template. If not specfied, STDOUT is used
notifycmd = "/etc/init.d/foo reload"
run command after template is regenerated (e.g restart xyz)
@ -171,9 +152,6 @@ path to a template to generate
watch = true
watch for container changes
wait = "500ms:2s"
debounce changes with a min:max duration. Only applicable if watch = true
[config.NotifyContainers]
Starts a notify container section
@ -201,18 +179,17 @@ watch = true
template = "/etc/docker-gen/templates/nginx.tmpl"
dest = "/etc/nginx/conf.d/default.conf"
watch = true
wait = "500ms:2s"
[config.NotifyContainers]
nginx = 1 # 1 is a signal number to be sent; here SIGHUP
e75a60548dc9 = 1 # a key can be either container name (nginx) or ID
nginx = 1 # 1 is a signal number to be sent; here SIGINT
e75a60548dc9 = 1 # a key can be either container name (nginx) or ID
```
===
### Templating
The templates used by docker-gen are written using the Go [text/template](http://golang.org/pkg/text/template/) language. In addition to the [built-in functions](http://golang.org/pkg/text/template/#hdr-Functions) supplied by Go, docker-gen uses [sprig](https://masterminds.github.io/sprig/) and some additional functions to make it simpler (or possible) to generate your desired output. Some templates rely on environment variables within the container to make decisions on what to generate from the template.
The templates used by docker-gen are written using the Go [text/template](http://golang.org/pkg/text/template/) language. In addition to the [built-in functions](http://golang.org/pkg/text/template/#hdr-Functions) supplied by Go, docker-gen provides a number of additional functions to make it simpler (or possible) to generate your desired output.
#### Emit Structure
@ -235,7 +212,6 @@ type RuntimeContainer struct {
IP6LinkLocal string
IP6Global string
Mounts []Mount
State State
}
type Address struct {
@ -287,21 +263,16 @@ type SwarmNode struct {
Address Address
}
type State struct {
Running bool
}
// Accessible from the root in templates as .Docker
type Docker struct {
Name string
NumContainers int
NumImages int
Version string
ApiVersion string
GoVersion string
OperatingSystem string
Architecture string
CurrentContainerID string
Name string
NumContainers int
NumImages int
Version string
ApiVersion string
GoVersion string
OperatingSystem string
Architecture string
}
// Host environment variables accessible from root in templates as .Env
@ -360,37 +331,31 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
#### Functions
* [Functions from Go](https://pkg.go.dev/text/template#hdr-Functions)
* [Functions from Sprig v3](https://masterminds.github.io/sprig/), except for those that have the same name as one of the following functions.
* *`closest $array $value`*: Returns the longest matching substring in `$array` that matches `$value`
* *`coalesce ...`*: Returns the first non-nil argument.
* *`contains $map $key`*: Returns `true` if `$map` contains `$key`. Takes maps from `string` to any type.
* *`contains $map $key`*: Returns `true` if `$map` contains `$key`. Takes maps from `string` to `string`.
* *`dict $key $value ...`*: Creates a map from a list of pairs. Each `$key` value must be a `string`, but the `$value` can be any type (or `nil`). Useful for passing more than one value as a pipeline context to subtemplates.
* *`dir $path`*: Returns an array of filenames in the specified `$path`.
* *`exists $path`*: Returns `true` if `$path` refers to an existing file or directory. Takes a string.
* *`eval $templateName [$data]`*: Evaluates the named template like Go's built-in `template` action, but instead of writing out the result it returns the result as a string so that it can be post-processed. The `$data` argument may be omitted, which is equivalent to passing `nil`.
* *`first $array`*: Returns the first value of an array or nil if the arry is nil or empty.
* *`groupBy $containers $fieldPath`*: Groups an array of `RuntimeContainer` instances based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value, which must be a string. Returns a map from the value of the field path expression to an array of containers having that value. Containers that do not have a value for the field path in question are omitted.
* *`groupByKeys $containers $fieldPath`*: Returns the same as `groupBy` but only returns the keys of the map.
* *`groupByMulti $containers $fieldPath $sep`*: Like `groupBy`, but the string value specified by `$fieldPath` is first split by `$sep` into a list of strings. A container whose `$fieldPath` value contains a list of strings will show up in the map output under each of those strings.
* *`groupByLabel $containers $label`*: Returns the same as `groupBy` but grouping by the given label's value.
* *`hasPrefix $prefix $string`*: Returns whether `$prefix` is a prefix of `$string`.
* *`hasSuffix $suffix $string`*: Returns whether `$suffix` is a suffix of `$string`.
* *`intersect $slice1 $slice2`*: Returns the strings that exist in both string slices.
* *`json $value`*: Returns the JSON representation of `$value` as a `string`.
* *`keys $map`*: Returns the keys from `$map`. If `$map` is `nil`, a `nil` is returned. If `$map` is not a `map`, an error will be thrown.
* *`last $array`*: Returns the last value of an array.
* *`parseBool $string`*: parseBool returns the boolean value represented by the string. It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False. Any other value returns an error. Alias for [`strconv.ParseBool`](http://golang.org/pkg/strconv/#ParseBool)
* *`replace $string $old $new $count`*: Replaces up to `$count` occurences of `$old` with `$new` in `$string`. Alias for [`strings.Replace`](http://golang.org/pkg/strings/#Replace)
* *`sha1 $string`*: Returns the hexadecimal representation of the SHA1 hash of `$string`.
* *`split $string $sep`*: Splits `$string` into a slice of substrings delimited by `$sep`. Alias for [`strings.Split`](http://golang.org/pkg/strings/#Split)
* *`splitN $string $sep $count`*: Splits `$string` into a slice of substrings delimited by `$sep`, with number of substrings returned determined by `$count`. Alias for [`strings.SplitN`](https://golang.org/pkg/strings/#SplitN)
* *`sortStringsAsc $strings`: Returns a slice of strings `$strings` sorted in ascending order.
* *`sortStringsDesc $strings`: Returns a slice of strings `$strings` sorted in descending (reverse) order.
* *`sortObjectsByKeysAsc $objects $fieldPath`: Returns the array `$objects`, sorted in ascending order based on the values of a field path expression `$fieldPath`.
* *`sortObjectsByKeysDesc $objects $fieldPath`: Returns the array `$objects`, sorted in descending (reverse) order based on the values of a field path expression `$fieldPath`.
* *`trimPrefix $prefix $string`*: If `$prefix` is a prefix of `$string`, return `$string` with `$prefix` trimmed from the beginning. Otherwise, return `$string` unchanged.
* *`trimSuffix $suffix $string`*: If `$suffix` is a suffix of `$string`, return `$string` with `$suffix` trimmed from the end. Otherwise, return `$string` unchanged.
* *`toLower $string`*: Replace capital letters in `$string` to lowercase.
* *`toUpper $string`*: Replace lowercase letters in `$string` to uppercase.
* *`trim $string`*: Removes whitespace from both sides of `$string`.
* *`when $condition $trueValue $falseValue`*: Returns the `$trueValue` when the `$condition` is `true` and the `$falseValue` otherwise
* *`where $items $fieldPath $value`*: Filters an array or slice based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value. Returns an array of items having that value.
* *`whereNot $items $fieldPath $value`*: Filters an array or slice based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value. Returns an array of items **not** having that value.
* *`whereExist $items $fieldPath`*: Like `where`, but returns only items where `$fieldPath` exists (is not nil).
* *`whereNotExist $items $fieldPath`*: Like `where`, but returns only items where `$fieldPath` does not exist (is nil).
* *`whereAny $items $fieldPath $sep $values`*: Like `where`, but the string value specified by `$fieldPath` is first split by `$sep` into a list of strings. The comparison value is a string slice with possible matches. Returns items which OR intersect these values.
@ -409,18 +374,18 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
#### NGINX Reverse Proxy Config
[nginxproxy/nginx-proxy](https://hub.docker.com/r/nginxproxy/nginx-proxy) trusted build.
[jwilder/nginx-proxy](https://index.docker.io/u/jwilder/nginx-proxy/) trusted build.
Start nginx-proxy:
```
$ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock -t nginxproxy/nginx-proxy
$ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock -t jwilder/nginx-proxy
```
Then start containers with a VIRTUAL_HOST (and the VIRTUAL_PORT if more than one port is exposed) env variable:
Then start containers with a VIRTUAL_HOST env variable:
```
$ docker run -e VIRTUAL_HOST=foo.bar.com -e VIRTUAL_PORT=80 -t ...
$ docker run -e VIRTUAL_HOST=foo.bar.com -t ...
```
If you wanted to run docker-gen directly on the host, you could do it with:
@ -431,7 +396,7 @@ $ docker-gen -only-published -watch -notify "/etc/init.d/nginx reload" templates
#### Fluentd Log Management
This template generate a fluentd.conf file used by fluentd. It would then ship log files off
This template generate a fluentd.conf file used by fluentd. It would then ships log files off
the host.
```
@ -441,8 +406,8 @@ $ docker-gen -watch -notify "restart fluentd" templates/fluentd.tmpl /etc/fluent
#### Service Discovery in Etcd
This template is an example of generating a script that is then executed. This template generates
a python script that is then executed which register containers in Etcd using its HTTP API.
This template is an example of generating a script that is then executed. This tempalte generates
a python script that is then executed which register containers in Etcd using it's HTTP API.
```
$ docker-gen -notify "/bin/bash /tmp/etcd.sh" -interval 10 templates/etcd.tmpl /tmp/etcd.sh
@ -451,11 +416,8 @@ $ docker-gen -notify "/bin/bash /tmp/etcd.sh" -interval 10 templates/etcd.tmpl /
### Development
This project uses [Go Modules](https://golang.org/ref/mod) for managing 3rd party dependencies.
This means that at least `go 1.11` is required.
For `go 1.11` and `go 1.12` it is additionally required to manually enable support by setting `GO111MODULE=on`.
For later versions, this is not required.
This project uses [glock](https://github.com/robfig/glock) for managing 3rd party dependencies.
You'll need to install glock into your workspace before hacking on docker-gen.
```
$ git clone <your fork>

View File

@ -5,39 +5,36 @@ import (
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"sync"
"github.com/BurntSushi/toml"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/generator"
"github.com/jwilder/docker-gen"
)
type stringslice []string
var (
buildVersion string
version bool
watch bool
wait string
notifyCmd string
notifyOutput bool
notifyContainerID string
notifyContainerSignal int
onlyExposed bool
onlyPublished bool
includeStopped bool
configFiles stringslice
configs config.ConfigFile
interval int
keepBlankLines bool
endpoint string
tlsCert string
tlsKey string
tlsCaCert string
tlsVerify bool
buildVersion string
version bool
watch bool
notifyCmd string
notifyOutput bool
notifySigHUPContainerID string
onlyExposed bool
onlyPublished bool
configFiles stringslice
configs dockergen.ConfigFile
interval int
keepBlankLines bool
endpoint string
tlsCert string
tlsKey string
tlsCaCert string
tlsVerify bool
tlsCertPath string
wg sync.WaitGroup
)
func (strings *stringslice) String() string {
@ -69,7 +66,7 @@ Environment Variables:
DOCKER_CERT_PATH - directory path containing key.pem, cert.pem and ca.pem
DOCKER_TLS_VERIFY - enable client TLS verification
`)
println(`For more information, see https://github.com/nginx-proxy/docker-gen`)
println(`For more information, see https://github.com/jwilder/docker-gen`)
}
func loadConfig(file string) error {
@ -88,20 +85,14 @@ func initFlags() {
}
flag.BoolVar(&version, "version", false, "show version")
flag.BoolVar(&watch, "watch", false, "watch for container changes")
flag.StringVar(&wait, "wait", "", "minimum and maximum durations to wait (e.g. \"500ms:2s\") before triggering generate")
flag.BoolVar(&onlyExposed, "only-exposed", false, "only include containers with exposed ports")
flag.BoolVar(&onlyPublished, "only-published", false,
"only include containers with published ports (implies -only-exposed)")
flag.BoolVar(&includeStopped, "include-stopped", false, "include stopped containers")
flag.BoolVar(&notifyOutput, "notify-output", false, "log the output(stdout/stderr) of notify command")
flag.StringVar(&notifyCmd, "notify", "", "run command after template is regenerated (e.g `restart xyz`)")
flag.StringVar(&notifyContainerID, "notify-sighup", "",
"send HUP signal to container. Equivalent to docker kill -s HUP `container-ID`")
flag.StringVar(&notifyContainerID, "notify-container", "",
"container to send a signal to")
flag.IntVar(&notifyContainerSignal, "notify-signal", int(docker.SIGHUP),
"signal to send to the notify-container. Defaults to SIGHUP")
flag.StringVar(&notifySigHUPContainerID, "notify-sighup", "",
"send HUP signal to container. Equivalent to `docker kill -s HUP container-ID`")
flag.Var(&configFiles, "config", "config files with template directives. Config files will be merged if this option is specified multiple times.")
flag.IntVar(&interval, "interval", 0, "notify command interval (secs)")
flag.BoolVar(&keepBlankLines, "keep-blank-lines", false, "keep blank lines in the output file")
@ -116,10 +107,6 @@ func initFlags() {
}
func main() {
// SIGHUP is used to trigger generation but go programs call os.Exit(2) at default.
// Ignore the signal until the handler is registered:
signal.Ignore(syscall.SIGHUP)
initFlags()
if version {
@ -136,57 +123,43 @@ func main() {
for _, configFile := range configFiles {
err := loadConfig(configFile)
if err != nil {
log.Fatalf("Error loading config %s: %s\n", configFile, err)
log.Fatalf("error loading config %s: %s\n", configFile, err)
}
}
} else {
w, err := config.ParseWait(wait)
if err != nil {
log.Fatalf("Error parsing wait interval: %s\n", err)
}
cfg := config.Config{
config := dockergen.Config{
Template: flag.Arg(0),
Dest: flag.Arg(1),
Watch: watch,
Wait: w,
NotifyCmd: notifyCmd,
NotifyOutput: notifyOutput,
NotifyContainers: make(map[string]int),
NotifyContainers: make(map[string]docker.Signal),
OnlyExposed: onlyExposed,
OnlyPublished: onlyPublished,
IncludeStopped: includeStopped,
Interval: interval,
KeepBlankLines: keepBlankLines,
}
if notifyContainerID != "" {
cfg.NotifyContainers[notifyContainerID] = notifyContainerSignal
if notifySigHUPContainerID != "" {
config.NotifyContainers[notifySigHUPContainerID] = docker.SIGHUP
}
configs = config.ConfigFile{
Config: []config.Config{cfg}}
configs = dockergen.ConfigFile{
Config: []dockergen.Config{config}}
}
all := true
for _, config := range configs.Config {
if config.IncludeStopped {
all = true
}
}
generator, err := generator.NewGenerator(generator.GeneratorConfig{
generator, err := dockergen.NewGenerator(dockergen.GeneratorConfig{
Endpoint: endpoint,
TLSKey: tlsKey,
TLSCert: tlsCert,
TLSCACert: tlsCaCert,
TLSVerify: tlsVerify,
All: all,
ConfigFile: configs,
})
if err != nil {
log.Fatalf("Error creating generator: %v", err)
log.Fatalf("error creating generator: %v", err)
}
if err := generator.Generate(); err != nil {
log.Fatalf("Error running generate: %v", err)
log.Fatalf("error running generate: %v", err)
}
}

33
config.go Normal file
View File

@ -0,0 +1,33 @@
package dockergen
import "github.com/fsouza/go-dockerclient"
type Config struct {
Template string
Dest string
Watch bool
NotifyCmd string
NotifyOutput bool
NotifyContainers map[string]docker.Signal
OnlyExposed bool
OnlyPublished bool
Interval int
KeepBlankLines bool
}
type ConfigFile struct {
Config []Config
}
func (c *ConfigFile) FilterWatches() ConfigFile {
configWithWatches := []Config{}
for _, config := range c.Config {
if config.Watch {
configWithWatches = append(configWithWatches, config)
}
}
return ConfigFile{
Config: configWithWatches,
}
}

150
context.go Normal file
View File

@ -0,0 +1,150 @@
package dockergen
import (
"os"
"sync"
"github.com/fsouza/go-dockerclient"
)
var (
mu sync.RWMutex
dockerInfo Docker
dockerEnv *docker.Env
)
type Context []*RuntimeContainer
func (c *Context) Env() map[string]string {
return splitKeyValueSlice(os.Environ())
}
func (c *Context) Docker() Docker {
mu.RLock()
defer mu.RUnlock()
return dockerInfo
}
func SetServerInfo(d *docker.Env) {
mu.Lock()
defer mu.Unlock()
dockerInfo = Docker{
Name: d.Get("Name"),
NumContainers: d.GetInt("Containers"),
NumImages: d.GetInt("Images"),
Version: dockerEnv.Get("Version"),
ApiVersion: dockerEnv.Get("ApiVersion"),
GoVersion: dockerEnv.Get("GoVersion"),
OperatingSystem: dockerEnv.Get("Os"),
Architecture: dockerEnv.Get("Arch"),
}
}
func SetDockerEnv(d *docker.Env) {
mu.Lock()
defer mu.Unlock()
dockerEnv = d
}
type Address struct {
IP string
IP6LinkLocal string
IP6Global string
Port string
HostPort string
Proto string
HostIP string
}
type Network struct {
IP string
Name string
Gateway string
EndpointID string
IPv6Gateway string
GlobalIPv6Address string
MacAddress string
GlobalIPv6PrefixLen int
IPPrefixLen int
}
type Volume struct {
Path string
HostPath string
ReadWrite bool
}
type RuntimeContainer struct {
ID string
Addresses []Address
Networks []Network
Gateway string
Name string
Hostname string
Image DockerImage
Env map[string]string
Volumes map[string]Volume
Node SwarmNode
Labels map[string]string
IP string
IP6LinkLocal string
IP6Global string
Mounts []Mount
}
func (r *RuntimeContainer) Equals(o RuntimeContainer) bool {
return r.ID == o.ID && r.Image == o.Image
}
func (r *RuntimeContainer) PublishedAddresses() []Address {
mapped := []Address{}
for _, address := range r.Addresses {
if address.HostPort != "" {
mapped = append(mapped, address)
}
}
return mapped
}
type DockerImage struct {
Registry string
Repository string
Tag string
}
func (i *DockerImage) String() string {
ret := i.Repository
if i.Registry != "" {
ret = i.Registry + "/" + i.Repository
}
if i.Tag != "" {
ret = ret + ":" + i.Tag
}
return ret
}
type SwarmNode struct {
ID string
Name string
Address Address
}
type Mount struct {
Name string
Source string
Destination string
Driver string
Mode string
RW bool
}
type Docker struct {
Name string
NumContainers int
NumImages int
Version string
ApiVersion string
GoVersion string
OperatingSystem string
Architecture string
}

View File

@ -1,4 +1,4 @@
package dockerclient
package dockergen
import (
"errors"
@ -8,33 +8,14 @@ import (
"strings"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
func GetEndpoint(endpoint string) (string, error) {
defaultEndpoint := "unix:///var/run/docker.sock"
if os.Getenv("DOCKER_HOST") != "" {
defaultEndpoint = os.Getenv("DOCKER_HOST")
}
if endpoint != "" {
defaultEndpoint = endpoint
}
_, _, err := parseHost(defaultEndpoint)
if err != nil {
return "", err
}
return defaultEndpoint, nil
}
func NewDockerClient(endpoint string, tlsVerify bool, tlsCert, tlsCaCert, tlsKey string) (*docker.Client, error) {
if strings.HasPrefix(endpoint, "unix:") {
return docker.NewClient(endpoint)
} else if tlsVerify || tlsEnabled(tlsCert, tlsCaCert, tlsKey) {
if tlsVerify {
if e, err := utils.PathExists(tlsCaCert); !e || err != nil {
if e, err := pathExists(tlsCaCert); !e || err != nil {
return nil, errors.New("TLS verification was requested, but CA cert does not exist")
}
}
@ -46,7 +27,7 @@ func NewDockerClient(endpoint string, tlsVerify bool, tlsCert, tlsCaCert, tlsKey
func tlsEnabled(tlsCert, tlsCaCert, tlsKey string) bool {
for _, v := range []string{tlsCert, tlsCaCert, tlsKey} {
if e, err := utils.PathExists(v); e && err == nil {
if e, err := pathExists(v); e && err == nil {
return true
}
}
@ -67,7 +48,7 @@ func parseHost(addr string) (string, string, error) {
addr = strings.TrimSpace(addr)
switch {
case addr == "tcp://":
return "", "", fmt.Errorf("invalid bind address format: %s", addr)
return "", "", fmt.Errorf("Invalid bind address format: %s", addr)
case strings.HasPrefix(addr, "unix://"):
proto = "unix"
addr = strings.TrimPrefix(addr, "unix://")
@ -84,7 +65,7 @@ func parseHost(addr string) (string, string, error) {
addr = "/var/run/docker.sock"
default:
if strings.Contains(addr, "://") {
return "", "", fmt.Errorf("invalid bind address protocol: %s", addr)
return "", "", fmt.Errorf("Invalid bind address protocol: %s", addr)
}
proto = "tcp"
}
@ -92,7 +73,7 @@ func parseHost(addr string) (string, string, error) {
if proto != "unix" && strings.Contains(addr, ":") {
hostParts := strings.Split(addr, ":")
if len(hostParts) != 2 {
return "", "", fmt.Errorf("invalid bind address format: %s", addr)
return "", "", fmt.Errorf("Invalid bind address format: %s", addr)
}
if hostParts[0] != "" {
host = hostParts[0]
@ -103,11 +84,11 @@ func parseHost(addr string) (string, string, error) {
if p, err := strconv.Atoi(hostParts[1]); err == nil && p != 0 {
port = p
} else {
return "", "", fmt.Errorf("invalid bind address format: %s", addr)
return "", "", fmt.Errorf("Invalid bind address format: %s", addr)
}
} else if proto == "tcp" && !strings.Contains(addr, ":") {
return "", "", fmt.Errorf("invalid bind address format: %s", addr)
return "", "", fmt.Errorf("Invalid bind address format: %s", addr)
} else {
host = addr
}
@ -118,7 +99,7 @@ func parseHost(addr string) (string, string, error) {
return proto, fmt.Sprintf("%s:%d", host, port), nil
}
func SplitDockerImage(img string) (string, string, string) {
func splitDockerImage(img string) (string, string, string) {
index := 0
repository := img
var registry, tag string
@ -137,3 +118,15 @@ func SplitDockerImage(img string) (string, string, string) {
return registry, repository, tag
}
// pathExists returns whether the given file or directory exists or not
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

196
docker_client_test.go Normal file
View File

@ -0,0 +1,196 @@
package dockergen
import (
"testing"
)
func TestSplitDockerImageRepository(t *testing.T) {
registry, repository, tag := splitDockerImage("ubuntu")
if registry != "" {
t.Fail()
}
if repository != "ubuntu" {
t.Fail()
}
if tag != "" {
t.Fail()
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "ubuntu" != dockerImage.String() {
t.Fail()
}
}
func TestSplitDockerImageWithRegistry(t *testing.T) {
registry, repository, tag := splitDockerImage("custom.registry/ubuntu")
if registry != "custom.registry" {
t.Fail()
}
if repository != "ubuntu" {
t.Fail()
}
if tag != "" {
t.Fail()
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "custom.registry/ubuntu" != dockerImage.String() {
t.Fail()
}
}
func TestSplitDockerImageWithRegistryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("custom.registry/ubuntu:12.04")
if registry != "custom.registry" {
t.Fail()
}
if repository != "ubuntu" {
t.Fail()
}
if tag != "12.04" {
t.Fail()
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "custom.registry/ubuntu:12.04" != dockerImage.String() {
t.Fail()
}
}
func TestSplitDockerImageWithRepositoryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("ubuntu:12.04")
if registry != "" {
t.Fail()
}
if repository != "ubuntu" {
t.Fail()
}
if tag != "12.04" {
t.Fail()
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "ubuntu:12.04" != dockerImage.String() {
t.Fail()
}
}
func TestSplitDockerImageWithPrivateRegistryPath(t *testing.T) {
registry, repository, tag := splitDockerImage("localhost:8888/ubuntu/foo:12.04")
if registry != "localhost:8888" {
t.Fail()
}
if repository != "ubuntu/foo" {
t.Fail()
}
if tag != "12.04" {
t.Fail()
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "localhost:8888/ubuntu/foo:12.04" != dockerImage.String() {
t.Fail()
}
}
func TestSplitDockerImageWithLocalRepositoryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("localhost:8888/ubuntu:12.04")
if registry != "localhost:8888" {
t.Fatalf("registry does not match: expected %s got %s", "localhost:8888", registry)
}
if repository != "ubuntu" {
t.Fatalf("repository does not match: expected %s got %s", "ubuntu", repository)
}
if tag != "12.04" {
t.Fatalf("tag does not match: expected %s got %s", "12.04", tag)
}
dockerImage := DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
if "localhost:8888/ubuntu:12.04" != dockerImage.String() {
t.Fail()
}
}
func TestParseHostUnix(t *testing.T) {
proto, addr, err := parseHost("unix:///var/run/docker.sock")
if err != nil {
t.Fatalf("%s", err)
}
if proto != "unix" || addr != "/var/run/docker.sock" {
t.Fatal("failed to parse unix:///var/run/docker.sock")
}
}
func TestParseHostUnixDefault(t *testing.T) {
proto, addr, err := parseHost("")
if err != nil {
t.Fatalf("%s", err)
}
if proto != "unix" || addr != "/var/run/docker.sock" {
t.Fatal("failed to parse ''")
}
}
func TestParseHostUnixDefaultNoPath(t *testing.T) {
proto, addr, err := parseHost("unix://")
if err != nil {
t.Fatalf("%s", err)
}
if proto != "unix" || addr != "/var/run/docker.sock" {
t.Fatal("failed to parse unix://")
}
}
func TestParseHostTCP(t *testing.T) {
proto, addr, err := parseHost("tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("%s", err)
}
if proto != "tcp" || addr != "127.0.0.1:4243" {
t.Fatal("failed to parse tcp://127.0.0.1:4243")
}
}
func TestParseHostTCPDefault(t *testing.T) {
proto, addr, err := parseHost("tcp://:4243")
if err != nil {
t.Fatalf("%s", err)
}
if proto != "tcp" || addr != "127.0.0.1:4243" {
t.Fatal("failed to parse unix:///var/run/docker.sock")
}
}

View File

@ -14,5 +14,5 @@ template = "/etc/docker-gen/templates/nginx.tmpl"
dest = "/etc/nginx/conf.d/default.conf"
watch = true
[config.NotifyContainers]
nginx = 1 # 1 is a signal number to be sent; here SIGHUP
nginx = 1 # 1 is a signal number to be sent; here SIGINT
e75a60548dc9 = 1 # a key can be either container name (nginx) or ID

View File

@ -1,6 +1,6 @@
[Unit]
Description=A file generator that renders templates using Docker Container meta-data.
Documentation=https://github.com/nginx-proxy/docker-gen
Documentation=https://github.com/jwilder/docker-gen
After=network.target docker.socket
Requires=docker.socket

383
generator.go Normal file
View File

@ -0,0 +1,383 @@
package dockergen
import (
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/fsouza/go-dockerclient"
)
type generator struct {
Client *docker.Client
Configs ConfigFile
Endpoint string
TLSVerify bool
TLSCert, TLSCaCert, TLSKey string
wg sync.WaitGroup
}
type GeneratorConfig struct {
Endpoint string
TLSCert string
TLSKey string
TLSCACert string
TLSVerify bool
ConfigFile ConfigFile
}
func NewGenerator(gc GeneratorConfig) (*generator, error) {
endpoint, err := GetEndpoint(gc.Endpoint)
if err != nil {
return nil, fmt.Errorf("Bad endpoint: %s", err)
}
client, err := NewDockerClient(endpoint, gc.TLSVerify, gc.TLSCert, gc.TLSCACert, gc.TLSKey)
if err != nil {
return nil, fmt.Errorf("Unable to create docker client: %s", err)
}
apiVersion, err := client.Version()
if err != nil {
log.Printf("error retrieving docker server version info: %s\n", err)
}
// Grab the docker daemon info once and hold onto it
SetDockerEnv(apiVersion)
return &generator{
Client: client,
Endpoint: gc.Endpoint,
TLSVerify: gc.TLSVerify,
TLSCert: gc.TLSCert,
TLSCaCert: gc.TLSCACert,
TLSKey: gc.TLSKey,
Configs: gc.ConfigFile,
}, nil
}
func (g *generator) Generate() error {
g.generateFromContainers()
g.generateAtInterval()
g.generateFromEvents()
g.generateFromSignals()
g.wg.Wait()
return nil
}
func (g *generator) generateFromSignals() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL)
g.wg.Add(1)
go func() {
defer g.wg.Done()
for {
sig := <-sigs
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGHUP:
g.generateFromContainers()
case syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT:
// exit when context is done
return
}
}
}()
}
func (g *generator) generateFromContainers() {
containers, err := g.getContainers()
if err != nil {
log.Printf("error listing containers: %s\n", err)
return
}
for _, config := range g.Configs.Config {
changed := GenerateFile(config, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
continue
}
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
}
}
func (g *generator) generateAtInterval() {
for _, config := range g.Configs.Config {
if config.Interval == 0 {
continue
}
log.Printf("Generating every %d seconds", config.Interval)
g.wg.Add(1)
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
quit := make(chan struct{})
go func(config Config) {
defer g.wg.Done()
for {
select {
case <-ticker.C:
containers, err := g.getContainers()
if err != nil {
log.Printf("Error listing containers: %s\n", err)
continue
}
// ignore changed return value. always run notify command
GenerateFile(config, containers)
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
case <-quit:
ticker.Stop()
return
}
}
}(config)
}
}
func (g *generator) generateFromEvents() {
configs := g.Configs.FilterWatches()
if len(configs.Config) == 0 {
return
}
g.wg.Add(1)
defer g.wg.Done()
client := g.Client
for {
if client == nil {
var err error
endpoint, err := GetEndpoint(g.Endpoint)
if err != nil {
log.Printf("Bad endpoint: %s", err)
time.Sleep(10 * time.Second)
continue
}
client, err = NewDockerClient(endpoint, g.TLSVerify, g.TLSCert, g.TLSCaCert, g.TLSKey)
if err != nil {
log.Printf("Unable to connect to docker daemon: %s", err)
time.Sleep(10 * time.Second)
continue
}
g.generateFromContainers()
}
eventChan := make(chan *docker.APIEvents, 100)
defer close(eventChan)
watching := false
for {
if client == nil {
break
}
err := client.Ping()
if err != nil {
log.Printf("Unable to ping docker daemon: %s", err)
if watching {
client.RemoveEventListener(eventChan)
watching = false
client = nil
}
time.Sleep(10 * time.Second)
break
}
if !watching {
err = client.AddEventListener(eventChan)
if err != nil && err != docker.ErrListenerAlreadyExists {
log.Printf("Error registering docker event listener: %s", err)
time.Sleep(10 * time.Second)
continue
}
watching = true
log.Println("Watching docker events")
}
select {
case event := <-eventChan:
if event == nil {
if watching {
client.RemoveEventListener(eventChan)
watching = false
client = nil
}
break
}
if event.Status == "start" || event.Status == "stop" || event.Status == "die" {
log.Printf("Received event %s for container %s", event.Status, event.ID[:12])
g.generateFromContainers()
}
case <-time.After(10 * time.Second):
// check for docker liveness
}
}
}
}
func (g *generator) runNotifyCmd(config Config) {
if config.NotifyCmd == "" {
return
}
log.Printf("Running '%s'", config.NotifyCmd)
cmd := exec.Command("/bin/sh", "-c", config.NotifyCmd)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Error running notify command: %s, %s\n", config.NotifyCmd, err)
}
if config.NotifyOutput {
for _, line := range strings.Split(string(out), "\n") {
if line != "" {
log.Printf("[%s]: %s", config.NotifyCmd, line)
}
}
}
}
func (g *generator) sendSignalToContainer(config Config) {
if len(config.NotifyContainers) < 1 {
return
}
for container, signal := range config.NotifyContainers {
log.Printf("Sending container '%s' signal '%v'", container, signal)
killOpts := docker.KillContainerOptions{
ID: container,
Signal: signal,
}
if err := g.Client.KillContainer(killOpts); err != nil {
log.Printf("Error sending signal to container: %s", err)
}
}
}
func (g *generator) getContainers() ([]*RuntimeContainer, error) {
apiInfo, err := g.Client.Info()
if err != nil {
log.Printf("error retrieving docker server info: %s\n", err)
}
SetServerInfo(apiInfo)
apiContainers, err := g.Client.ListContainers(docker.ListContainersOptions{
All: false,
Size: false,
})
if err != nil {
return nil, err
}
containers := []*RuntimeContainer{}
for _, apiContainer := range apiContainers {
container, err := g.Client.InspectContainer(apiContainer.ID)
if err != nil {
log.Printf("error inspecting container: %s: %s\n", apiContainer.ID, err)
continue
}
registry, repository, tag := splitDockerImage(container.Config.Image)
runtimeContainer := &RuntimeContainer{
ID: container.ID,
Image: DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
},
Name: strings.TrimLeft(container.Name, "/"),
Hostname: container.Config.Hostname,
Gateway: container.NetworkSettings.Gateway,
Addresses: []Address{},
Networks: []Network{},
Env: make(map[string]string),
Volumes: make(map[string]Volume),
Node: SwarmNode{},
Labels: make(map[string]string),
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
}
for k, v := range container.NetworkSettings.Ports {
address := Address{
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
Port: k.Port(),
Proto: k.Proto(),
}
if len(v) > 0 {
address.HostPort = v[0].HostPort
address.HostIP = v[0].HostIP
}
runtimeContainer.Addresses = append(runtimeContainer.Addresses,
address)
}
for k, v := range container.NetworkSettings.Networks {
network := Network{
IP: v.IPAddress,
Name: k,
Gateway: v.Gateway,
EndpointID: v.EndpointID,
IPv6Gateway: v.IPv6Gateway,
GlobalIPv6Address: v.GlobalIPv6Address,
MacAddress: v.MacAddress,
GlobalIPv6PrefixLen: v.GlobalIPv6PrefixLen,
IPPrefixLen: v.IPPrefixLen,
}
runtimeContainer.Networks = append(runtimeContainer.Networks,
network)
}
for k, v := range container.Volumes {
runtimeContainer.Volumes[k] = Volume{
Path: k,
HostPath: v,
ReadWrite: container.VolumesRW[k],
}
}
if container.Node != nil {
runtimeContainer.Node.ID = container.Node.ID
runtimeContainer.Node.Name = container.Node.Name
runtimeContainer.Node.Address = Address{
IP: container.Node.IP,
}
}
for _, v := range container.Mounts {
runtimeContainer.Mounts = append(runtimeContainer.Mounts, Mount{
Name: v.Name,
Source: v.Source,
Destination: v.Destination,
Driver: v.Driver,
Mode: v.Mode,
RW: v.RW,
})
}
runtimeContainer.Env = splitKeyValueSlice(container.Config.Env)
runtimeContainer.Labels = container.Config.Labels
containers = append(containers, runtimeContainer)
}
return containers, nil
}

50
go.mod
View File

@ -1,50 +0,0 @@
module github.com/nginx-proxy/docker-gen
go 1.20
require (
github.com/BurntSushi/toml v1.3.2
github.com/Masterminds/sprig/v3 v3.2.3
github.com/fsouza/go-dockerclient v1.10.0
github.com/stretchr/testify v1.8.4
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/containerd/containerd v1.6.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/klauspost/compress v1.11.13 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

198
go.sum
View File

@ -1,198 +0,0 @@
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.9.6 h1:VwnDOgLeoi2du6dAznfmspNqTiwczvjv4K7NxuY9jsY=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns=
github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsouza/go-dockerclient v1.10.0 h1:ppSBsbR60I1DFbV4Ag7LlHlHakHFRNLk9XakATW1yVQ=
github.com/fsouza/go-dockerclient v1.10.0/go.mod h1:+iNzAW78AzClIBTZ6WFjkaMvOgz68GyCJ236b1opLTs=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=

View File

@ -1,83 +0,0 @@
package config
import (
"errors"
"strings"
"time"
)
type Config struct {
Template string
Dest string
Watch bool
Wait *Wait
NotifyCmd string
NotifyOutput bool
NotifyContainers map[string]int
OnlyExposed bool
OnlyPublished bool
IncludeStopped bool
Interval int
KeepBlankLines bool
}
type ConfigFile struct {
Config []Config
}
func (c *ConfigFile) FilterWatches() ConfigFile {
configWithWatches := []Config{}
for _, config := range c.Config {
if config.Watch {
configWithWatches = append(configWithWatches, config)
}
}
return ConfigFile{
Config: configWithWatches,
}
}
type Wait struct {
Min time.Duration
Max time.Duration
}
func (w *Wait) UnmarshalText(text []byte) error {
wait, err := ParseWait(string(text))
if err == nil {
w.Min, w.Max = wait.Min, wait.Max
}
return err
}
func ParseWait(s string) (*Wait, error) {
if len(strings.TrimSpace(s)) < 1 {
return &Wait{0, 0}, nil
}
parts := strings.Split(s, ":")
var (
min time.Duration
max time.Duration
err error
)
min, err = time.ParseDuration(strings.TrimSpace(parts[0]))
if err != nil {
return nil, err
}
if len(parts) > 1 {
max, err = time.ParseDuration(strings.TrimSpace(parts[1]))
if err != nil {
return nil, err
}
if max < min {
return nil, errors.New("invalid wait interval: max must be larger than min")
}
} else {
max = 4 * min
}
return &Wait{min, max}, nil
}

View File

@ -1,61 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterWatches(t *testing.T) {
testConfigFile := &ConfigFile{
Config: []Config{
{Template: "foo", Watch: true},
{Template: "bar"},
{Template: "baz", Watch: true},
},
}
expected := []Config{
{Template: "foo", Watch: true},
{Template: "baz", Watch: true},
}
configFile := testConfigFile.FilterWatches()
assert.Equal(t, expected, configFile.Config)
}
func TestParseWait(t *testing.T) {
incorrectIntervals := []string{
"500x", // Incorrect min interval
"500s:4x", // Incorrect max interval
"1m:1s", // Min interval larger than max interval
}
for _, intervalString := range incorrectIntervals {
wait, err := ParseWait(intervalString)
assert.Error(t, err)
assert.Nil(t, wait)
}
correctIntervals := map[string]Wait{
"": {0, 0}, // Empty time interval string
"1ms": {1000000, 4000000}, // Correct min interval without max
"1ms:111ms": {1000000, 111000000}, // Correct min:max time interval
}
for intervalString, expectedWait := range correctIntervals {
wait, err := ParseWait(intervalString)
assert.NoError(t, err)
assert.Equal(t, &expectedWait, wait)
}
}
func TestWaitUnmarshalText(t *testing.T) {
// Correct min:max time interval
intervalBytes := []byte("1ms:2ms")
expectedWait := &Wait{1000000, 2000000}
wait := new(Wait)
err := wait.UnmarshalText(intervalBytes)
assert.NoError(t, err)
assert.Equal(t, expectedWait, wait)
}

View File

@ -1,242 +0,0 @@
package context
import (
"bufio"
"fmt"
"os"
"regexp"
"sync"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
var (
mu sync.RWMutex
dockerInfo Docker
dockerEnv *docker.Env
)
type Context []*RuntimeContainer
func (c *Context) Env() map[string]string {
return utils.SplitKeyValueSlice(os.Environ())
}
func (c *Context) Docker() Docker {
mu.RLock()
defer mu.RUnlock()
return dockerInfo
}
func SetServerInfo(d *docker.DockerInfo) {
mu.Lock()
defer mu.Unlock()
dockerInfo = Docker{
Name: d.Name,
NumContainers: d.Containers,
NumImages: d.Images,
Version: dockerEnv.Get("Version"),
ApiVersion: dockerEnv.Get("ApiVersion"),
GoVersion: dockerEnv.Get("GoVersion"),
OperatingSystem: dockerEnv.Get("Os"),
Architecture: dockerEnv.Get("Arch"),
CurrentContainerID: GetCurrentContainerID(),
}
}
func SetDockerEnv(d *docker.Env) {
mu.Lock()
defer mu.Unlock()
dockerEnv = d
}
type Address struct {
IP string
IP6LinkLocal string
IP6Global string
Port string
HostPort string
Proto string
HostIP string
}
type Network struct {
IP string
Name string
Gateway string
EndpointID string
IPv6Gateway string
GlobalIPv6Address string
MacAddress string
GlobalIPv6PrefixLen int
IPPrefixLen int
}
type Volume struct {
Path string
HostPath string
ReadWrite bool
}
type State struct {
Running bool
}
type RuntimeContainer struct {
ID string
Addresses []Address
Networks []Network
Gateway string
Name string
Hostname string
Image DockerImage
Env map[string]string
Volumes map[string]Volume
Node SwarmNode
Labels map[string]string
IP string
IP6LinkLocal string
IP6Global string
Mounts []Mount
State State
}
func (r *RuntimeContainer) Equals(o RuntimeContainer) bool {
return r.ID == o.ID && r.Image == o.Image
}
func (r *RuntimeContainer) PublishedAddresses() []Address {
mapped := []Address{}
for _, address := range r.Addresses {
if address.HostPort != "" {
mapped = append(mapped, address)
}
}
return mapped
}
type DockerImage struct {
Registry string
Repository string
Tag string
}
func (i *DockerImage) String() string {
ret := i.Repository
if i.Registry != "" {
ret = i.Registry + "/" + i.Repository
}
if i.Tag != "" {
ret = ret + ":" + i.Tag
}
return ret
}
type SwarmNode struct {
ID string
Name string
Address Address
}
type Mount struct {
Name string
Source string
Destination string
Driver string
Mode string
RW bool
}
type Docker struct {
Name string
NumContainers int
NumImages int
Version string
ApiVersion string
GoVersion string
OperatingSystem string
Architecture string
CurrentContainerID string
}
// GetCurrentContainerID attempts to extract the current container ID from the provided file paths.
// If no files paths are provided, it will default to /proc/1/cpuset, /proc/self/cgroup and /proc/self/mountinfo.
// It attempts to match the HOSTNAME first then use the fallback method, and returns with the first valid match.
func GetCurrentContainerID(filepaths ...string) (id string) {
if len(filepaths) == 0 {
filepaths = []string{"/proc/1/cpuset", "/proc/self/cgroup", "/proc/self/mountinfo"}
}
// We try to match a 64 character hex string starting with the hostname first
for _, filepath := range filepaths {
file, err := os.Open(filepath)
if err != nil {
continue
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
_, lines, err := bufio.ScanLines([]byte(scanner.Text()), true)
if err == nil {
strLines := string(lines)
if id = matchContainerIDWithHostname(strLines); len(id) == 64 {
return
}
}
}
}
// If we didn't get any ID that matches the hostname, fall back to matching the first 64 character hex string
for _, filepath := range filepaths {
file, err := os.Open(filepath)
if err != nil {
continue
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
_, lines, err := bufio.ScanLines([]byte(scanner.Text()), true)
if err == nil {
strLines := string(lines)
if id = matchContainerID("([[:alnum:]]{64})", strLines); len(id) == 64 {
return
}
}
}
}
return
}
func matchContainerIDWithHostname(lines string) string {
hostname := os.Getenv("HOSTNAME")
re := regexp.MustCompilePOSIX("^[[:alnum:]]{12}$")
if re.MatchString(hostname) {
regex := fmt.Sprintf("(%s[[:alnum:]]{52})", hostname)
return matchContainerID(regex, lines)
}
return ""
}
func matchContainerID(regex, lines string) string {
// Attempt to detect if we're on a line from a /proc/<pid>/mountinfo file and modify the regexp accordingly
// https://www.kernel.org/doc/Documentation/filesystems/proc.txt section 3.5
re := regexp.MustCompilePOSIX("^[0-9]+ [0-9]+ [0-9]+:[0-9]+ /")
if re.MatchString(lines) {
regex = fmt.Sprintf("containers/%v", regex)
}
re = regexp.MustCompilePOSIX(regex)
if re.MatchString(lines) {
submatches := re.FindStringSubmatch(string(lines))
containerID := submatches[1]
return containerID
}
return ""
}

View File

@ -1,200 +0,0 @@
package context
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
var (
ids = []string{
"0fa939e22e6938e7517f663de83e79a5087a18b1b997a36e0c933a917cddb295",
"e881f8c51a72db7da515e9d5cab8ed105b869579eb9923fdcf4ee80933160802",
"eede6bd9e72f5d783a4bfb845bd71f310e974cb26987328a5d15704e23a8d6cb",
}
fileKeys = []string{
"cpuset",
"cgroup",
"mountinfo",
}
contents = map[string]string{
"cpuset": fmt.Sprintf("/docker/%v", ids[0]),
"cgroup": fmt.Sprintf(`13:name=systemd:/docker-ce/docker/%[1]v
12:pids:/docker-ce/docker/%[1]v
11:hugetlb:/docker-ce/docker/%[1]v
10:net_prio:/docker-ce/docker/%[1]v
9:perf_event:/docker-ce/docker/%[1]v
8:net_cls:/docker-ce/docker/%[1]v
7:freezer:/docker-ce/docker/%[1]v
6:devices:/docker-ce/docker/%[1]v
5:memory:/docker-ce/docker/%[1]v
4:blkio:/docker-ce/docker/%[1]v
3:cpuacct:/docker-ce/docker/%[1]v
2:cpu:/docker-ce/docker/%[1]v
1:cpuset:/docker-ce/docker/%[1]v`, ids[1]),
"mountinfo": fmt.Sprintf(`705 661 0:96 / / rw,relatime master:192 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/CVAK3VWZFQCUGTLHRJHPEKJ4UL:/var/lib/docker/overlay2/l/XMJZ73SKVWVECU7TJCOY62F3H2:/var/lib/docker/overlay2/l/AVNBXO52GHDY3MZU3R4RCSNMCE:/var/lib/docker/overlay2/l/L4IJZ33E6NAMXJ5W3SKJSVX5TS:/var/lib/docker/overlay2/l/JXAUAD5TDJCXA34FGS6NYGUZKT:/var/lib/docker/overlay2/l/TBQDSAFKBSTFMUS3QCFWN5NRLB:/var/lib/docker/overlay2/l/MXIUXRGB7MU4Y4NUNZE2VXTXIN:/var/lib/docker/overlay2/l/HN7E4YWJG7TMG7BXLZTGICTBOA:/var/lib/docker/overlay2/l/65XQPC72Z5VRY4THGASZIQXS57:/var/lib/docker/overlay2/l/BVQKC7LU6D7MOSLBDKFHY7YSO3:/var/lib/docker/overlay2/l/R4GGX3SFPMLXTNM3WKMVOKDTOY:/var/lib/docker/overlay2/l/VHGYTU73JLTRCGX45ZF2VGW4FK,upperdir=/var/lib/docker/overlay2/e1fab975d5ffd51474b11a964c82c3bfda1c0e82aec6845a1f12c8150bf61419/diff,workdir=/var/lib/docker/overlay2/e1fab975d5ffd51474b11a964c82c3bfda1c0e82aec6845a1f12c8150bf61419/work,index=off
706 705 0:105 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
707 705 0:106 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
708 707 0:107 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
709 705 0:108 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
710 709 0:25 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot
711 707 0:104 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
712 707 0:109 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
713 705 8:3 /var/lib/docker/containers/%[1]v/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda3 rw
714 705 8:3 /var/lib/docker/containers/%[1]v/hostname /etc/hostname rw,relatime - ext4 /dev/sda3 rw
715 705 8:3 /var/lib/docker/containers/%[1]v/hosts /etc/hosts rw,relatime - ext4 /dev/sda3 rw
716 705 8:3 /var/lib/docker/volumes/ca8074e1a2eb12edc86c59c5108bb48c31bb7ace4b90beb0da8137a9baa45812/_data /etc/nginx/certs rw,relatime master:1 - ext4 /dev/sda3 rw
717 705 8:3 /var/lib/docker/volumes/2cf8a52c907469a56f6e2cc7d1959d74a4dd04131e7edcd53eaf909db28f770f/_data /etc/nginx/dhparam rw,relatime master:1 - ext4 /dev/sda3 rw
662 707 0:107 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
663 706 0:105 /bus /proc/bus ro,relatime - proc proc rw
664 706 0:105 /fs /proc/fs ro,relatime - proc proc rw
665 706 0:105 /irq /proc/irq ro,relatime - proc proc rw
666 706 0:105 /sys /proc/sys ro,relatime - proc proc rw
667 706 0:105 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw
668 706 0:110 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64
669 706 0:106 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
670 706 0:106 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
671 706 0:106 /null /proc/latency_stats rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
672 706 0:106 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
673 706 0:106 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
674 706 0:111 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64
675 709 0:112 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64`, ids[2]),
}
)
func TestGetCurrentContainerID(t *testing.T) {
hostname := os.Getenv("HOSTNAME")
defer os.Setenv("HOSTNAME", hostname)
var filepaths []string
// Create temporary files with test content
for _, key := range fileKeys {
file, err := os.CreateTemp("", key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
if _, err = file.WriteString(contents[key]); err != nil {
t.Fatal(err)
}
filepaths = append(filepaths, file.Name())
}
// Each time the HOSTNAME is set to a short form ID, GetCurrentContainerID() should match and return the corresponding full ID
for _, id := range ids {
os.Setenv("HOSTNAME", id[0:12])
assert.Equal(t, id, GetCurrentContainerID(filepaths...), "id mismatch with default HOSTNAME")
}
// If the Hostname isn't a short form ID, we should match the first valid ID (64 character hex string) instead
os.Setenv("HOSTNAME", "customhostname")
assert.Equal(t, ids[0], GetCurrentContainerID(filepaths...), "id mismatch with custom HOSTNAME")
}
func TestGetCurrentContainerIDMountInfo(t *testing.T) {
// Test specific to cases like https://github.com/nginx-proxy/docker-gen/issues/355
// where only the /proc/<pid>/mountinfo file contains information
hostname := os.Getenv("HOSTNAME")
defer os.Setenv("HOSTNAME", hostname)
os.Setenv("HOSTNAME", "customhostname")
id := ids[2]
content := map[string]string{
"cpuset": "/",
"cgroup": "0::/",
"mountinfo": contents["mountinfo"],
}
var filepaths []string
// Create temporary files with test content
for _, key := range fileKeys {
file, err := os.CreateTemp("", key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
if _, err = file.WriteString(content[key]); err != nil {
t.Fatal(err)
}
filepaths = append(filepaths, file.Name())
}
// We should match the correct 64 characters long ID in mountinfo, not the first encountered
assert.Equal(t, id, GetCurrentContainerID(filepaths...), "id mismatch on mountinfo")
}
func TestGetCurrentContainerEmpty(t *testing.T) {
assert.Equal(t, "", GetCurrentContainerID())
}
func TestPublishedAddresses(t *testing.T) {
container := &RuntimeContainer{
Addresses: []Address{
{
IP: "172.19.0.1",
HostPort: "80",
},
{
IP: "172.19.0.2",
},
{
IP: "172.19.0.3",
HostPort: "8080",
},
},
}
expected := []Address{
{
IP: "172.19.0.1",
HostPort: "80",
},
{
IP: "172.19.0.3",
HostPort: "8080",
},
}
assert.ElementsMatch(t, expected, container.PublishedAddresses())
}
func TestRuntimeContainerEquals(t *testing.T) {
rc1 := &RuntimeContainer{
ID: "baz",
Image: DockerImage{
Registry: "foo/bar",
},
}
rc2 := &RuntimeContainer{
ID: "baz",
Name: "qux",
Image: DockerImage{
Registry: "foo/bar",
},
}
assert.True(t, rc1.Equals(*rc2))
assert.True(t, rc2.Equals(*rc1))
rc2.Image.Tag = "quux"
assert.False(t, rc1.Equals(*rc2))
assert.False(t, rc2.Equals(*rc1))
}
func TestDockerImageString(t *testing.T) {
image := &DockerImage{Repository: "foo/bar"}
assert.Equal(t, "foo/bar", image.String())
image.Registry = "baz.io"
assert.Equal(t, "baz.io/foo/bar", image.String())
image.Tag = "qux"
assert.Equal(t, "baz.io/foo/bar:qux", image.String())
image.Registry = ""
assert.Equal(t, "foo/bar:qux", image.String())
}

View File

@ -1,248 +0,0 @@
package dockerclient
import (
"fmt"
"os"
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestDefaultEndpoint(t *testing.T) {
err := os.Unsetenv("DOCKER_HOST")
if err != nil {
t.Fatalf("Unable to unset DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "unix:///var/run/docker.sock" {
t.Fatalf("Expected unix:///var/run/docker.sock, got %s", endpoint)
}
}
func TestDockerHostEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:4243" {
t.Fatalf("Expected tcp://127.0.0.1:4243, got %s", endpoint)
}
}
func TestDockerFlagEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
// flag value should override DOCKER_HOST and default value
endpoint, err := GetEndpoint("tcp://127.0.0.1:5555")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:5555" {
t.Fatalf("Expected tcp://127.0.0.1:5555, got %s", endpoint)
}
}
func TestUnixBadFormat(t *testing.T) {
endpoint := "unix:/var/run/docker.sock"
_, err := GetEndpoint(endpoint)
if err == nil {
t.Fatal("endpoint should have failed")
}
}
func TestSplitDockerImageRepository(t *testing.T) {
registry, repository, tag := SplitDockerImage("ubuntu")
assert.Equal(t, "", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "ubuntu", dockerImage.String())
}
func TestSplitDockerImageWithRegistry(t *testing.T) {
registry, repository, tag := SplitDockerImage("custom.registry/ubuntu")
assert.Equal(t, "custom.registry", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "custom.registry/ubuntu", dockerImage.String())
}
func TestSplitDockerImageWithRegistryAndTag(t *testing.T) {
registry, repository, tag := SplitDockerImage("custom.registry/ubuntu:12.04")
assert.Equal(t, "custom.registry", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "custom.registry/ubuntu:12.04", dockerImage.String())
}
func TestSplitDockerImageWithRepositoryAndTag(t *testing.T) {
registry, repository, tag := SplitDockerImage("ubuntu:12.04")
assert.Equal(t, "", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "ubuntu:12.04", dockerImage.String())
}
func TestSplitDockerImageWithPrivateRegistryPath(t *testing.T) {
registry, repository, tag := SplitDockerImage("localhost:8888/ubuntu/foo:12.04")
assert.Equal(t, "localhost:8888", registry)
assert.Equal(t, "ubuntu/foo", repository)
assert.Equal(t, "12.04", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "localhost:8888/ubuntu/foo:12.04", dockerImage.String())
}
func TestSplitDockerImageWithLocalRepositoryAndTag(t *testing.T) {
registry, repository, tag := SplitDockerImage("localhost:8888/ubuntu:12.04")
assert.Equal(t, "localhost:8888", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
}
assert.Equal(t, "localhost:8888/ubuntu:12.04", dockerImage.String())
}
func TestParseHostUnix(t *testing.T) {
proto, addr, err := parseHost("unix:///var/run/docker.sock")
assert.NoError(t, err)
assert.Equal(t, "unix", proto, "failed to parse unix:///var/run/docker.sock")
assert.Equal(t, "/var/run/docker.sock", addr, "failed to parse unix:///var/run/docker.sock")
}
func TestParseHostUnixDefault(t *testing.T) {
proto, addr, err := parseHost("")
assert.NoError(t, err)
assert.Equal(t, "unix", proto, "failed to parse ''")
assert.Equal(t, "/var/run/docker.sock", addr, "failed to parse ''")
}
func TestParseHostUnixDefaultNoPath(t *testing.T) {
proto, addr, err := parseHost("unix://")
assert.NoError(t, err)
assert.Equal(t, "unix", proto, "failed to parse unix://")
assert.Equal(t, "/var/run/docker.sock", addr, "failed to parse unix://")
}
func TestParseHostTCP(t *testing.T) {
proto, addr, err := parseHost("tcp://127.0.0.1:4243")
assert.NoError(t, err)
assert.Equal(t, "tcp", proto, "failed to parse tcp://127.0.0.1:4243")
assert.Equal(t, "127.0.0.1:4243", addr, "failed to parse tcp://127.0.0.1:4243")
}
func TestParseHostTCPDefault(t *testing.T) {
proto, addr, err := parseHost("tcp://:4243")
assert.NoError(t, err)
assert.Equal(t, "tcp", proto, "failed to parse tcp://:4243")
assert.Equal(t, "127.0.0.1:4243", addr, "failed to parse tcp://:4243")
}
func TestParseHostSystemd(t *testing.T) {
proto, addr, err := parseHost("fd://")
assert.NoError(t, err)
assert.Equal(t, "fd", proto, "failed to parse fd://")
assert.Equal(t, "fd://", addr, "failed to parse fd://")
}
func assertParseHostError(t *testing.T, address string) {
proto, addr, err := parseHost(address)
message := fmt.Sprintf("should have failed to parse %v", address)
assert.Error(t, err, message)
assert.Equal(t, "", proto, message)
assert.Equal(t, "", addr, message)
}
func TestParseHostTCPNoAddressError(t *testing.T) {
assertParseHostError(t, "tcp://")
}
func TestParseHostTCPIncorrectBindAddressError(t *testing.T) {
incorrectBindAdresses := []string{
"tcp://127.0.0.1:4243:80",
"tcp://127.0.0.1:",
"tcp://127.0.0.1",
}
for _, address := range incorrectBindAdresses {
assertParseHostError(t, address)
}
}
func TestParseHostWrongProtocolError(t *testing.T) {
assertParseHostError(t, "foo://")
}
func TestTlsEnabled(t *testing.T) {
tls := tlsEnabled("foo", "bar", "baz")
assert.False(t, tls)
filepaths := map[string]string{
"cert": "",
"caCert": "",
"key": "",
}
// Create temporary files
for key := range filepaths {
file, err := os.CreateTemp("", key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
filepaths[key] = file.Name()
}
tls = tlsEnabled(filepaths["cert"], filepaths["caCert"], filepaths["key"])
assert.True(t, tls)
}

View File

@ -1,524 +0,0 @@
package generator
import (
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/dockerclient"
"github.com/nginx-proxy/docker-gen/internal/template"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
type generator struct {
Client *docker.Client
Configs config.ConfigFile
Endpoint string
TLSVerify bool
TLSCert, TLSCaCert, TLSKey string
All bool
wg sync.WaitGroup
retry bool
}
type GeneratorConfig struct {
Endpoint string
TLSCert string
TLSKey string
TLSCACert string
TLSVerify bool
All bool
ConfigFile config.ConfigFile
}
func NewGenerator(gc GeneratorConfig) (*generator, error) {
endpoint, err := dockerclient.GetEndpoint(gc.Endpoint)
if err != nil {
return nil, fmt.Errorf("bad endpoint: %s", err)
}
client, err := dockerclient.NewDockerClient(endpoint, gc.TLSVerify, gc.TLSCert, gc.TLSCACert, gc.TLSKey)
if err != nil {
return nil, fmt.Errorf("unable to create docker client: %s", err)
}
apiVersion, err := client.Version()
if err != nil {
log.Printf("Error retrieving docker server version info: %s\n", err)
}
// Grab the docker daemon info once and hold onto it
context.SetDockerEnv(apiVersion)
return &generator{
Client: client,
Endpoint: gc.Endpoint,
TLSVerify: gc.TLSVerify,
TLSCert: gc.TLSCert,
TLSCaCert: gc.TLSCACert,
TLSKey: gc.TLSKey,
All: gc.All,
Configs: gc.ConfigFile,
retry: true,
}, nil
}
func (g *generator) Generate() error {
g.generateFromContainers()
g.generateAtInterval()
g.generateFromEvents()
g.generateFromSignals()
g.wg.Wait()
return nil
}
func (g *generator) generateFromSignals() {
var hasWatcher bool
for _, config := range g.Configs.Config {
if config.Watch {
hasWatcher = true
break
}
}
// If none of the configs need to watch for events, don't watch for signals either
if !hasWatcher {
return
}
g.wg.Add(1)
go func() {
defer g.wg.Done()
sigChan, cleanup := newSignalChannel()
defer cleanup()
for {
sig := <-sigChan
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGHUP:
g.generateFromContainers()
case syscall.SIGTERM, syscall.SIGINT:
// exit when context is done
return
}
}
}()
}
func (g *generator) generateFromContainers() {
containers, err := g.getContainers()
if err != nil {
log.Printf("Error listing containers: %s\n", err)
return
}
for _, config := range g.Configs.Config {
changed := template.GenerateFile(config, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
continue
}
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
}
}
func (g *generator) generateAtInterval() {
for _, cfg := range g.Configs.Config {
if cfg.Interval == 0 {
continue
}
log.Printf("Generating every %d seconds", cfg.Interval)
g.wg.Add(1)
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
go func(cfg config.Config) {
defer g.wg.Done()
sigChan, cleanup := newSignalChannel()
defer cleanup()
for {
select {
case <-ticker.C:
containers, err := g.getContainers()
if err != nil {
log.Printf("Error listing containers: %s\n", err)
continue
}
// ignore changed return value. always run notify command
template.GenerateFile(cfg, containers)
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
case sig := <-sigChan:
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGTERM, syscall.SIGINT:
ticker.Stop()
return
}
}
}
}(cfg)
}
}
func (g *generator) generateFromEvents() {
configs := g.Configs.FilterWatches()
if len(configs.Config) == 0 {
return
}
client := g.Client
var watchers []chan *docker.APIEvents
for _, cfg := range configs.Config {
if !cfg.Watch {
continue
}
g.wg.Add(1)
watcher := make(chan *docker.APIEvents, 100)
watchers = append(watchers, watcher)
go func(cfg config.Config) {
defer g.wg.Done()
debouncedChan := newDebounceChannel(watcher, cfg.Wait)
for range debouncedChan {
containers, err := g.getContainers()
if err != nil {
log.Printf("Error listing containers: %s\n", err)
continue
}
changed := template.GenerateFile(cfg, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", cfg.Dest, cfg.NotifyCmd)
continue
}
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
}
}(cfg)
}
// maintains docker client connection and passes events to watchers
go func() {
// channel will be closed by go-dockerclient
eventChan := make(chan *docker.APIEvents, 100)
sigChan, cleanup := newSignalChannel()
defer cleanup()
for {
watching := false
if client == nil {
var err error
endpoint, err := dockerclient.GetEndpoint(g.Endpoint)
if err != nil {
log.Printf("Bad endpoint: %s", err)
time.Sleep(10 * time.Second)
continue
}
client, err = dockerclient.NewDockerClient(endpoint, g.TLSVerify, g.TLSCert, g.TLSCaCert, g.TLSKey)
if err != nil {
log.Printf("Unable to connect to docker daemon: %s", err)
time.Sleep(10 * time.Second)
continue
}
}
for {
if client == nil {
break
}
if !watching {
err := client.AddEventListener(eventChan)
if err != nil && err != docker.ErrListenerAlreadyExists {
log.Printf("Error registering docker event listener: %s", err)
time.Sleep(10 * time.Second)
continue
}
watching = true
log.Println("Watching docker events")
// sync all configs after resuming listener
g.generateFromContainers()
}
select {
case event, ok := <-eventChan:
if !ok {
log.Printf("Docker daemon connection interrupted")
if watching {
client.RemoveEventListener(eventChan)
watching = false
client = nil
}
if !g.retry {
// close all watchers and exit
for _, watcher := range watchers {
close(watcher)
}
return
}
// recreate channel and attempt to resume
eventChan = make(chan *docker.APIEvents, 100)
time.Sleep(10 * time.Second)
break
}
if event.Status == "start" || event.Status == "stop" || event.Status == "die" {
log.Printf("Received event %s for container %s", event.Status, event.ID[:12])
// fanout event to all watchers
for _, watcher := range watchers {
watcher <- event
}
}
case <-time.After(10 * time.Second):
// check for docker liveness
err := client.Ping()
if err != nil {
log.Printf("Unable to ping docker daemon: %s", err)
if watching {
client.RemoveEventListener(eventChan)
watching = false
client = nil
}
}
case sig := <-sigChan:
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGTERM, syscall.SIGINT:
// close all watchers and exit
for _, watcher := range watchers {
close(watcher)
}
return
}
}
}
}
}()
}
func (g *generator) runNotifyCmd(config config.Config) {
if config.NotifyCmd == "" {
return
}
log.Printf("Running '%s'", config.NotifyCmd)
cmd := exec.Command("/bin/sh", "-c", config.NotifyCmd)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Error running notify command: %s, %s\n", config.NotifyCmd, err)
}
if config.NotifyOutput {
for _, line := range strings.Split(string(out), "\n") {
if line != "" {
log.Printf("[%s]: %s", config.NotifyCmd, line)
}
}
}
}
func (g *generator) sendSignalToContainer(config config.Config) {
if len(config.NotifyContainers) < 1 {
return
}
for container, signal := range config.NotifyContainers {
log.Printf("Sending container '%s' signal '%v'", container, signal)
if signal == -1 {
if err := g.Client.RestartContainer(container, 10); err != nil {
log.Printf("Error sending restarting container: %s", err)
}
return
}
killOpts := docker.KillContainerOptions{
ID: container,
Signal: docker.Signal(signal),
}
if err := g.Client.KillContainer(killOpts); err != nil {
log.Printf("Error sending signal to container: %s", err)
}
}
}
func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
apiInfo, err := g.Client.Info()
if err != nil {
log.Printf("Error retrieving docker server info: %s\n", err)
} else {
context.SetServerInfo(apiInfo)
}
apiContainers, err := g.Client.ListContainers(docker.ListContainersOptions{
All: g.All,
Size: false,
})
if err != nil {
return nil, err
}
containers := []*context.RuntimeContainer{}
for _, apiContainer := range apiContainers {
opts := docker.InspectContainerOptions{ID: apiContainer.ID}
container, err := g.Client.InspectContainerWithOptions(opts)
if err != nil {
log.Printf("Error inspecting container: %s: %s\n", apiContainer.ID, err)
continue
}
registry, repository, tag := dockerclient.SplitDockerImage(container.Config.Image)
runtimeContainer := &context.RuntimeContainer{
ID: container.ID,
Image: context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
},
State: context.State{
Running: container.State.Running,
},
Name: strings.TrimLeft(container.Name, "/"),
Hostname: container.Config.Hostname,
Gateway: container.NetworkSettings.Gateway,
Addresses: []context.Address{},
Networks: []context.Network{},
Env: make(map[string]string),
Volumes: make(map[string]context.Volume),
Node: context.SwarmNode{},
Labels: make(map[string]string),
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
}
for k, v := range container.NetworkSettings.Ports {
address := context.Address{
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
Port: k.Port(),
Proto: k.Proto(),
}
if len(v) > 0 {
address.HostPort = v[0].HostPort
address.HostIP = v[0].HostIP
}
runtimeContainer.Addresses = append(runtimeContainer.Addresses,
address)
}
for k, v := range container.NetworkSettings.Networks {
network := context.Network{
IP: v.IPAddress,
Name: k,
Gateway: v.Gateway,
EndpointID: v.EndpointID,
IPv6Gateway: v.IPv6Gateway,
GlobalIPv6Address: v.GlobalIPv6Address,
MacAddress: v.MacAddress,
GlobalIPv6PrefixLen: v.GlobalIPv6PrefixLen,
IPPrefixLen: v.IPPrefixLen,
}
runtimeContainer.Networks = append(runtimeContainer.Networks,
network)
}
for k, v := range container.Volumes {
runtimeContainer.Volumes[k] = context.Volume{
Path: k,
HostPath: v,
ReadWrite: container.VolumesRW[k],
}
}
if container.Node != nil {
runtimeContainer.Node.ID = container.Node.ID
runtimeContainer.Node.Name = container.Node.Name
runtimeContainer.Node.Address = context.Address{
IP: container.Node.IP,
}
}
for _, v := range container.Mounts {
runtimeContainer.Mounts = append(runtimeContainer.Mounts, context.Mount{
Name: v.Name,
Source: v.Source,
Destination: v.Destination,
Driver: v.Driver,
Mode: v.Mode,
RW: v.RW,
})
}
runtimeContainer.Env = utils.SplitKeyValueSlice(container.Config.Env)
runtimeContainer.Labels = container.Config.Labels
containers = append(containers, runtimeContainer)
}
return containers, nil
}
func newSignalChannel() (<-chan os.Signal, func()) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
return sig, func() { signal.Stop(sig) }
}
func newDebounceChannel(input chan *docker.APIEvents, wait *config.Wait) chan *docker.APIEvents {
if wait == nil {
return input
}
if wait.Min == 0 {
return input
}
output := make(chan *docker.APIEvents, 100)
go func() {
var (
event *docker.APIEvents
minTimer <-chan time.Time
maxTimer <-chan time.Time
)
defer close(output)
for {
select {
case buffer, ok := <-input:
if !ok {
return
}
event = buffer
minTimer = time.After(wait.Min)
if maxTimer == nil {
maxTimer = time.After(wait.Max)
}
case <-minTimer:
log.Println("Debounce minTimer fired")
minTimer, maxTimer = nil, nil
output <- event
case <-maxTimer:
log.Println("Debounce maxTimer fired")
minTimer, maxTimer = nil, nil
output <- event
}
}
}()
return output
}

View File

@ -1,211 +0,0 @@
package generator
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync/atomic"
"testing"
"time"
docker "github.com/fsouza/go-dockerclient"
dockertest "github.com/fsouza/go-dockerclient/testing"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/dockerclient"
)
func TestGenerateFromEvents(t *testing.T) {
log.SetOutput(io.Discard)
containerID := "8dfafdbc3a40"
var counter atomic.Int32
eventsResponse := `
{"status":"start","id":"8dfafdbc3a40","from":"base:latest","time":1374067924}
{"status":"stop","id":"8dfafdbc3a40","from":"base:latest","time":1374067966}
{"status":"start","id":"8dfafdbc3a40","from":"base:latest","time":1374067970}
{"status":"destroy","id":"8dfafdbc3a40","from":"base:latest","time":1374067990}`
infoResponse := `{"Containers":1,"Images":1,"Debug":0,"NFd":11,"NGoroutines":21,"MemoryLimit":1,"SwapLimit":0}`
versionResponse := `{"Version":"1.8.0","Os":"Linux","KernelVersion":"3.18.5-tinycore64","GoVersion":"go1.4.1","GitCommit":"a8a31ef","Arch":"amd64","ApiVersion":"1.19"}`
server, _ := dockertest.NewServer("127.0.0.1:0", nil, nil)
server.CustomHandler("/events", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rsc := bufio.NewScanner(strings.NewReader(eventsResponse))
for rsc.Scan() {
w.Write([]byte(rsc.Text()))
w.(http.Flusher).Flush()
time.Sleep(150 * time.Millisecond)
}
time.Sleep(500 * time.Millisecond)
}))
server.CustomHandler("/info", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(infoResponse))
w.(http.Flusher).Flush()
}))
server.CustomHandler("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(versionResponse))
w.(http.Flusher).Flush()
}))
server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := []docker.APIContainers{
{
ID: containerID,
Image: "base:latest",
Command: "/bin/sh",
Created: time.Now().Unix(),
Status: "running",
Ports: []docker.APIPort{},
Names: []string{"/docker-gen-test"},
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}))
server.CustomHandler(fmt.Sprintf("/containers/%s/json", containerID), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
counter := counter.Add(1)
container := docker.Container{
Name: "docker-gen-test",
ID: containerID,
Created: time.Now(),
Path: "/bin/sh",
Args: []string{},
Config: &docker.Config{
Hostname: "docker-gen",
AttachStdout: true,
AttachStderr: true,
Env: []string{fmt.Sprintf("COUNTER=%d", counter)},
Cmd: []string{"/bin/sh"},
Image: "base:latest",
},
State: docker.State{
Running: true,
Pid: 400,
ExitCode: 0,
StartedAt: time.Now(),
},
Image: "0ff407d5a7d9ed36acdf3e75de8cc127afecc9af234d05486be2981cdc01a38d",
NetworkSettings: &docker.NetworkSettings{
IPAddress: "10.0.0.10",
IPPrefixLen: 24,
Gateway: "10.0.0.1",
Bridge: "docker0",
PortMapping: map[string]docker.PortMapping{},
Ports: map[docker.Port][]docker.PortBinding{},
},
ResolvConfPath: "/etc/resolv.conf",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(container)
}))
serverURL := fmt.Sprintf("tcp://%s", strings.TrimRight(strings.TrimPrefix(server.URL(), "http://"), "/"))
client, err := dockerclient.NewDockerClient(serverURL, false, "", "", "")
if err != nil {
t.Errorf("Failed to create client: %s", err)
}
client.SkipServerVersionCheck = true
tmplFile, err := os.CreateTemp(os.TempDir(), "docker-gen-tmpl")
if err != nil {
t.Errorf("Failed to create temp file: %v\n", err)
}
defer func() {
tmplFile.Close()
os.Remove(tmplFile.Name())
}()
err = os.WriteFile(tmplFile.Name(), []byte("{{range $key, $value := .}}{{$value.ID}}.{{$value.Env.COUNTER}}{{end}}"), 0644)
if err != nil {
t.Errorf("Failed to write to temp file: %v\n", err)
}
var destFiles []*os.File
for i := 0; i < 4; i++ {
destFile, err := os.CreateTemp(os.TempDir(), "docker-gen-out")
if err != nil {
t.Errorf("Failed to create temp file: %v\n", err)
}
destFiles = append(destFiles, destFile)
}
defer func() {
for _, destFile := range destFiles {
destFile.Close()
os.Remove(destFile.Name())
}
}()
apiVersion, err := client.Version()
if err != nil {
t.Errorf("Failed to retrieve docker server version info: %v\n", err)
}
context.SetDockerEnv(apiVersion) // prevents a panic
generator := &generator{
Client: client,
Endpoint: serverURL,
Configs: config.ConfigFile{
Config: []config.Config{
{
Template: tmplFile.Name(),
Dest: destFiles[0].Name(),
Watch: false,
},
{
Template: tmplFile.Name(),
Dest: destFiles[1].Name(),
Watch: true,
Wait: &config.Wait{Min: 0, Max: 0},
},
{
Template: tmplFile.Name(),
Dest: destFiles[2].Name(),
Watch: true,
Wait: &config.Wait{Min: 200 * time.Millisecond, Max: 250 * time.Millisecond},
},
{
Template: tmplFile.Name(),
Dest: destFiles[3].Name(),
Watch: true,
Wait: &config.Wait{Min: 250 * time.Millisecond, Max: 1 * time.Second},
},
},
},
retry: false,
}
generator.generateFromEvents()
generator.wg.Wait()
var (
value []byte
expected string
)
// The counter is incremented in each output file in the following sequence:
//
// init 150ms 200ms 250ms 300ms 350ms 400ms 450ms 500ms 550ms 600ms 650ms 700ms
// ├──────╫──────┼──────┼──────╫──────┼──────┼──────╫──────┼──────┼──────┼──────┼──────┤
// File0 ├─ 1 ║ ║ ║
// File1 ├─ 1 ╟─ 2 ╟─ 3 ╟─ 5
// File2 ├─ 1 ╟───── max (250ms) ──║───────────> 4 ╟─────── min (200ms) ─────> 6
// File3 └─ 1 ╟──────────────────> ╟──────────────────> ╟─────────── min (250ms) ────────> 7
// ┌───╨───┐ ┌───╨──┐ ┌───╨───┐
// │ start │ │ stop │ │ start │
// └───────┘ └──────┘ └───────┘
expectedCounters := []int{1, 5, 6, 7}
for i, counter := range expectedCounters {
value, _ = os.ReadFile(destFiles[i].Name())
expected = fmt.Sprintf("%s.%d", containerID, counter)
if string(value) != expected {
t.Errorf("expected: %s. got: %s", expected, value)
}
}
}

View File

@ -1,165 +0,0 @@
package template
import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"log"
"os"
"reflect"
"strings"
)
func keys(input interface{}) (interface{}, error) {
if input == nil {
return nil, nil
}
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot call keys on a non-map value: %v", input)
}
vk := val.MapKeys()
k := make([]interface{}, val.Len())
for i := range k {
k[i] = vk[i].Interface()
}
return k, nil
}
func intersect(l1, l2 []string) []string {
m := make(map[string]bool)
m2 := make(map[string]bool)
for _, v := range l2 {
m2[v] = true
}
for _, v := range l1 {
if m2[v] {
m[v] = true
}
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func contains(input interface{}, key interface{}) bool {
if input == nil {
return false
}
val := reflect.ValueOf(input)
if val.Kind() == reflect.Map {
for _, k := range val.MapKeys() {
if k.Interface() == key {
return true
}
}
}
return false
}
func hashSha1(input string) string {
h := sha1.New()
io.WriteString(h, input)
return fmt.Sprintf("%x", h.Sum(nil))
}
func marshalJson(input interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(input); err != nil {
return "", err
}
return strings.TrimSuffix(buf.String(), "\n"), nil
}
func unmarshalJson(input string) (interface{}, error) {
var v interface{}
if err := json.Unmarshal([]byte(input), &v); err != nil {
return nil, err
}
return v, nil
}
// arrayClosest find the longest matching substring in values
// that matches input
func arrayClosest(values []string, input string) string {
best := ""
for _, v := range values {
if strings.Contains(input, v) && len(v) > len(best) {
best = v
}
}
return best
}
// dirList returns a list of files in the specified path
func dirList(path string) ([]string, error) {
names := []string{}
files, err := os.ReadDir(path)
if err != nil {
log.Printf("Template error: %v", err)
return names, nil
}
for _, f := range files {
names = append(names, f.Name())
}
return names, nil
}
// coalesce returns the first non nil argument
func coalesce(input ...interface{}) interface{} {
for _, v := range input {
if v != nil {
return v
}
}
return nil
}
// coalesceempty returns the first non nil argument or empty
func coalesceempty(input ...interface{}) interface{} {
for _, v := range input {
if v != nil && len(fmt.Sprintf("%v", v)) > 0 {
return v
}
}
return nil
}
// trimPrefix returns a string without the prefix, if present
func trimPrefix(prefix, s string) string {
return strings.TrimPrefix(s, prefix)
}
// trimSuffix returns a string without the suffix, if present
func trimSuffix(suffix, s string) string {
return strings.TrimSuffix(s, suffix)
}
// toLower return the string in lower case
func toLower(s string) string {
return strings.ToLower(s)
}
// toUpper return the string in upper case
func toUpper(s string) string {
return strings.ToUpper(s)
}
// when returns the trueValue when the condition is true and the falseValue otherwise
func when(condition bool, trueValue, falseValue interface{}) interface{} {
if condition {
return trueValue
} else {
return falseValue
}
}

View File

@ -1,300 +0,0 @@
package template
import (
"bytes"
"encoding/json"
"os"
"path"
"reflect"
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestContainsString(t *testing.T) {
env := map[string]string{
"PORT": "1234",
}
assert.True(t, contains(env, "PORT"))
assert.False(t, contains(env, "MISSING"))
}
func TestContainsInteger(t *testing.T) {
env := map[int]int{
42: 1234,
}
assert.True(t, contains(env, 42))
assert.False(t, contains(env, "WRONG TYPE"))
assert.False(t, contains(env, 24))
}
func TestContainsNilInput(t *testing.T) {
var env interface{} = nil
assert.False(t, contains(env, 0))
assert.False(t, contains(env, ""))
}
func TestKeys(t *testing.T) {
env := map[string]string{
"VIRTUAL_HOST": "demo.local",
}
tests := templateTestList{
{`{{range (keys $)}}{{.}}{{end}}`, env, `VIRTUAL_HOST`},
}
tests.run(t)
}
func TestKeysEmpty(t *testing.T) {
input := map[string]int{}
k, err := keys(input)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() == reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
if len(input) != vk.Len() {
t.Fatalf("Incorrect key count; expected %d, got %d", len(input), vk.Len())
}
}
func TestKeysNil(t *testing.T) {
k, err := keys(nil)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() != reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
}
func TestIntersect(t *testing.T) {
i := intersect([]string{"foo.fo.com", "bar.com"}, []string{"foo.bar.com"})
assert.Len(t, i, 0, "Expected no match")
i = intersect([]string{"foo.fo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.fo.com", "foo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 2, "Expected exactly two matches")
}
func TestSplitN(t *testing.T) {
tests := templateTestList{
{`{{index (splitN . "/" 2) 0}}`, "example.com/path", `example.com`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/path", `path`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/a/longer/path", `a/longer/path`},
{`{{len (splitN . "/" 2)}}`, "example.com", `1`},
}
tests.run(t)
}
func TestTrimPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
const trimmed = "127.0.0.1:2375"
got := trimPrefix(prefix, str)
if got != trimmed {
t.Fatalf("expected trimPrefix(%s,%s) to be %s, got %s", prefix, str, trimmed, got)
}
}
func TestTrimSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
const trimmed = "myhost"
got := trimSuffix(suffix, str)
if got != trimmed {
t.Fatalf("expected trimSuffix(%s,%s) to be %s, got %s", suffix, str, trimmed, got)
}
}
func TestToLower(t *testing.T) {
const str = ".RaNd0m StrinG_"
const lowered = ".rand0m string_"
assert.Equal(t, lowered, toLower(str), "Unexpected value from toLower()")
}
func TestToUpper(t *testing.T) {
const str = ".RaNd0m StrinG_"
const uppered = ".RAND0M STRING_"
assert.Equal(t, uppered, toUpper(str), "Unexpected value from toUpper()")
}
func TestSha1(t *testing.T) {
sum := hashSha1("/path")
if sum != "4f26609ad3f5185faaa9edf1e93aa131e2131352" {
t.Fatal("Incorrect SHA1 sum")
}
}
func TestJson(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
output, err := marshalJson(containers)
if err != nil {
t.Fatal(err)
}
buf := bytes.NewBufferString(output)
dec := json.NewDecoder(buf)
if err != nil {
t.Fatal(err)
}
var decoded []*context.RuntimeContainer
if err := dec.Decode(&decoded); err != nil {
t.Fatal(err)
}
if len(decoded) != len(containers) {
t.Fatalf("Incorrect unmarshaled container count. Expected %d, got %d.", len(containers), len(decoded))
}
}
func TestParseJson(t *testing.T) {
tests := templateTestList{
{`{{parseJson .}}`, `null`, `<no value>`},
{`{{parseJson .}}`, `true`, `true`},
{`{{parseJson .}}`, `1`, `1`},
{`{{parseJson .}}`, `0.5`, `0.5`},
{`{{index (parseJson .) "enabled"}}`, `{"enabled":true}`, `true`},
{`{{index (parseJson . | first) "enabled"}}`, `[{"enabled":true}]`, `true`},
}
tests.run(t)
}
func TestQueryEscape(t *testing.T) {
tests := templateTestList{
{`{{queryEscape .}}`, `example.com`, `example.com`},
{`{{queryEscape .}}`, `.example.com`, `.example.com`},
{`{{queryEscape .}}`, `*.example.com`, `%2A.example.com`},
{`{{queryEscape .}}`, `~^example\.com(\..*\.xip\.io)?$`, `~%5Eexample%5C.com%28%5C..%2A%5C.xip%5C.io%29%3F%24`},
}
tests.run(t)
}
func TestArrayClosestExact(t *testing.T) {
if arrayClosest([]string{"foo.bar.com", "bar.com"}, "foo.bar.com") != "foo.bar.com" {
t.Fatal("Expected foo.bar.com")
}
}
func TestArrayClosestSubstring(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bar.com"}, "foo.bar.com") != "bar.com" {
t.Fatal("Expected bar.com")
}
}
func TestArrayClosestNoMatch(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bip.com"}, "foo.bar.com") != "" {
t.Fatal("Expected ''")
}
}
func TestWhen(t *testing.T) {
context := struct {
BoolValue bool
StringValue string
}{
true,
"foo",
}
tests := templateTestList{
{`{{ print (when .BoolValue "first" "second") }}`, context, `first`},
{`{{ print (when (eq .StringValue "foo") "first" "second") }}`, context, `first`},
{`{{ when (not .BoolValue) "first" "second" | print }}`, context, `second`},
{`{{ when (not (eq .StringValue "foo")) "first" "second" | print }}`, context, `second`},
}
tests.run(t)
}
func TestWhenTrue(t *testing.T) {
if when(true, "first", "second") != "first" {
t.Fatal("Expected first value")
}
}
func TestWhenFalse(t *testing.T) {
if when(false, "first", "second") != "second" {
t.Fatal("Expected second value")
}
}
func TestDirList(t *testing.T) {
dir, err := os.MkdirTemp("", "dirList")
if err != nil {
t.Fatal(err)
}
defer os.Remove(dir)
files := map[string]string{
"aaa": "",
"bbb": "",
"ccc": "",
}
// Create temporary files
for key := range files {
file, err := os.CreateTemp(dir, key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
files[key] = file.Name()
}
expected := []string{
path.Base(files["aaa"]),
path.Base(files["bbb"]),
path.Base(files["ccc"]),
}
filesList, _ := dirList(dir)
assert.Equal(t, expected, filesList)
filesList, _ = dirList("/wrong/path")
assert.Equal(t, []string{}, filesList)
}
func TestCoalesce(t *testing.T) {
v := coalesce(nil, "second", "third")
assert.Equal(t, "second", v, "Expected second value")
v = coalesce(nil, nil, nil)
assert.Nil(t, v, "Expected nil value")
}

View File

@ -1,88 +0,0 @@
package template
import (
"fmt"
"strings"
"github.com/nginx-proxy/docker-gen/internal/context"
)
// Generalized groupBy function
func generalizedGroupBy(funcName string, entries interface{}, getValue func(interface{}) (interface{}, error), addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
groups := make(map[string][]interface{})
for i := 0; i < entriesVal.Len(); i++ {
v := entriesVal.Index(i).Interface()
value, err := getValue(v)
if err != nil {
return nil, err
}
if value != nil {
addEntry(groups, value, v)
}
}
return groups, nil
}
func generalizedGroupByKey(funcName string, entries interface{}, key string, addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
getKey := func(v interface{}) (interface{}, error) {
return deepGet(v, key), nil
}
return generalizedGroupBy(funcName, entries, getKey, addEntry)
}
func groupByMulti(entries interface{}, key, sep string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupByMulti", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
items := strings.Split(value.(string), sep)
for _, item := range items {
if item != "" {
groups[item] = append(groups[item], v)
}
}
})
}
// groupBy groups a generic array or slice by the path property key
func groupBy(entries interface{}, key string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupBy", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
// groupByKeys is the same as groupBy but only returns a list of keys
func groupByKeys(entries interface{}, key string) ([]string, error) {
keys, err := generalizedGroupByKey("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
if err != nil {
return nil, err
}
ret := []string{}
for k := range keys {
ret = append(ret, k)
}
return ret, nil
}
// groupByLabel is the same as groupBy but over a given label
func groupByLabel(entries interface{}, label string) (map[string][]interface{}, error) {
getLabel := func(v interface{}) (interface{}, error) {
if container, ok := v.(*context.RuntimeContainer); ok {
if value, ok := container.Labels[label]; ok {
return value, nil
}
return nil, nil
}
return nil, fmt.Errorf("must pass an array or slice of *RuntimeContainer to 'groupByLabel'; received %v", v)
}
return generalizedGroupBy("groupByLabel", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}

View File

@ -1,205 +0,0 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestGroupByExistingKey(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 2)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(*context.RuntimeContainer).ID)
}
func TestGroupByAfterWhere(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
}
filtered, _ := where(containers, "Env.EXTERNAL", "true")
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 1)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(*context.RuntimeContainer).ID)
}
func TestGroupByKeys(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
expected := []string{"demo1.localhost", "demo2.localhost"}
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
expected = []string{"1", "2", "3"}
groups, err = groupByKeys(containers, "ID")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
}
func TestGeneralizedGroupByError(t *testing.T) {
groups, err := groupBy("string", "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByLabel(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
{
ID: "4",
},
{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}
groups, err := groupByLabel(containers, "com.docker.compose.project")
assert.NoError(t, err)
assert.Len(t, groups, 3)
assert.Len(t, groups["one"], 2)
assert.Len(t, groups[""], 1)
assert.Len(t, groups["two"], 1)
assert.Equal(t, "2", groups["two"][0].(*context.RuntimeContainer).ID)
}
func TestGroupByLabelError(t *testing.T) {
strings := []string{"foo", "bar", "baz"}
groups, err := groupByLabel(strings, "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByMulti(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, _ := groupByMulti(containers, "Env.VIRTUAL_HOST", ",")
if len(groups) != 3 {
t.Fatalf("expected 3 got %d", len(groups))
}
if len(groups["demo1.localhost"]) != 2 {
t.Fatalf("expected 2 got %d", len(groups["demo1.localhost"]))
}
if len(groups["demo2.localhost"]) != 1 {
t.Fatalf("expected 1 got %d", len(groups["demo2.localhost"]))
}
if groups["demo2.localhost"][0].(*context.RuntimeContainer).ID != "3" {
t.Fatalf("expected 2 got %s", groups["demo2.localhost"][0].(*context.RuntimeContainer).ID)
}
if len(groups["demo3.localhost"]) != 1 {
t.Fatalf("expect 1 got %d", len(groups["demo3.localhost"]))
}
if groups["demo3.localhost"][0].(*context.RuntimeContainer).ID != "2" {
t.Fatalf("expected 2 got %s", groups["demo3.localhost"][0].(*context.RuntimeContainer).ID)
}
}

View File

@ -1,57 +0,0 @@
package template
import (
"log"
"math"
"reflect"
"strconv"
"strings"
)
func deepGetImpl(v reflect.Value, path []string) interface{} {
if !v.IsValid() {
return nil
}
if len(path) == 0 {
return v.Interface()
}
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if v.Kind() == reflect.Pointer {
log.Printf("unable to descend into pointer of a pointer\n")
return nil
}
switch v.Kind() {
case reflect.Struct:
return deepGetImpl(v.FieldByName(path[0]), path[1:])
case reflect.Map:
return deepGetImpl(v.MapIndex(reflect.ValueOf(path[0])), path[1:])
case reflect.Slice, reflect.Array:
iu64, err := strconv.ParseUint(path[0], 10, 64)
if err != nil {
log.Printf("non-negative decimal number required for array/slice index, got %#v\n", path[0])
return nil
}
if iu64 > math.MaxInt {
iu64 = math.MaxInt
}
i := int(iu64)
if i >= v.Len() {
log.Printf("index %v out of bounds", i)
return nil
}
return deepGetImpl(v.Index(i), path[1:])
default:
log.Printf("unable to index by %s (value %v, kind %s)\n", path[0], v, v.Kind())
return nil
}
}
func deepGet(item interface{}, path string) interface{} {
var parts []string
if path != "" {
parts = strings.Split(strings.TrimPrefix(path, "."), ".")
}
return deepGetImpl(reflect.ValueOf(item), parts)
}

View File

@ -1,95 +0,0 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestDeepGetNoPath(t *testing.T) {
item := context.RuntimeContainer{}
value := deepGet(item, "")
if _, ok := value.(context.RuntimeContainer); !ok {
t.Fail()
}
returned := value.(context.RuntimeContainer)
if !returned.Equals(item) {
t.Fail()
}
}
func TestDeepGetSimple(t *testing.T) {
item := context.RuntimeContainer{
ID: "expected",
}
value := deepGet(item, "ID")
assert.IsType(t, "", value)
assert.Equal(t, "expected", value)
}
func TestDeepGetSimpleDotPrefix(t *testing.T) {
item := context.RuntimeContainer{
ID: "expected",
}
value := deepGet(item, ".ID")
assert.IsType(t, "", value)
assert.Equal(t, "expected", value)
}
func TestDeepGetMap(t *testing.T) {
item := context.RuntimeContainer{
Env: map[string]string{
"key": "value",
},
}
value := deepGet(item, "Env.key")
assert.IsType(t, "", value)
assert.Equal(t, "value", value)
}
func TestDeepGet(t *testing.T) {
s := struct{ X string }{"foo"}
sp := &s
for _, tc := range []struct {
desc string
item interface{}
path string
want interface{}
}{
{
"map key empty string",
map[string]map[string]map[string]string{
"": {
"": {
"": "foo",
},
},
},
"...",
"foo",
},
{"struct", s, "X", "foo"},
{"pointer to struct", sp, "X", "foo"},
{"double pointer to struct", &sp, ".X", nil},
{"slice index", []string{"foo", "bar"}, "1", "bar"},
{"slice index out of bounds", []string{}, "0", nil},
{"slice index negative", []string{}, "-1", nil},
{"slice index nonnumber", []string{}, "foo", nil},
{"array index", [2]string{"foo", "bar"}, "1", "bar"},
{"array index out of bounds", [1]string{"foo"}, "1", nil},
{"array index negative", [1]string{"foo"}, "-1", nil},
{"array index nonnumber", [1]string{"foo"}, "foo", nil},
} {
t.Run(tc.desc, func(t *testing.T) {
got := deepGet(tc.item, tc.path)
assert.IsType(t, tc.want, got)
assert.Equal(t, tc.want, got)
})
}
}

View File

@ -1,90 +0,0 @@
package template
import (
"reflect"
"sort"
)
// sortStrings returns a sorted array of strings in increasing order
func sortStringsAsc(values []string) []string {
sort.Strings(values)
return values
}
// sortStringsDesc returns a sorted array of strings in decreasing order
func sortStringsDesc(values []string) []string {
sort.Sort(sort.Reverse(sort.StringSlice(values)))
return values
}
type sortable interface {
sort.Interface
set(string, interface{}) error
get() []interface{}
}
type sortableData struct {
data []interface{}
}
func (s sortableData) get() []interface{} {
return s.data
}
func (s sortableData) Len() int { return len(s.data) }
func (s sortableData) Swap(i, j int) { s.data[i], s.data[j] = s.data[j], s.data[i] }
type sortableByKey struct {
sortableData
key string
}
func (s *sortableByKey) set(funcName string, entries interface{}) (err error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return
}
s.data = make([]interface{}, entriesVal.Len())
for i := 0; i < entriesVal.Len(); i++ {
s.data[i] = entriesVal.Index(i).Interface()
}
return
}
// method required to implement sort.Interface
func (s sortableByKey) Less(i, j int) bool {
values := map[int]string{i: "", j: ""}
for k := range values {
if v := reflect.ValueOf(deepGet(s.data[k], s.key)); v.Kind() != reflect.Invalid {
values[k] = v.Interface().(string)
}
}
return values[i] < values[j]
}
// Generalized SortBy function
func generalizedSortBy(funcName string, entries interface{}, s sortable, reverse bool) (sorted []interface{}, err error) {
err = s.set(funcName, entries)
if err != nil {
return nil, err
}
if reverse {
sort.Stable(sort.Reverse(s))
} else {
sort.Stable(s)
}
return s.get(), nil
}
// sortObjectsByKeysAsc returns a sorted array of objects, sorted by object's key field in ascending order
func sortObjectsByKeysAsc(objs interface{}, key string) ([]interface{}, error) {
s := &sortableByKey{key: key}
return generalizedSortBy("sortObjsByKeys", objs, s, false)
}
// sortObjectsByKeysDesc returns a sorted array of objects, sorted by object's key field in descending order
func sortObjectsByKeysDesc(objs interface{}, key string) ([]interface{}, error) {
s := &sortableByKey{key: key}
return generalizedSortBy("sortObjsByKey", objs, s, true)
}

View File

@ -1,66 +0,0 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestSortStringsAsc(t *testing.T) {
strings := []string{"foo", "bar", "baz", "qux"}
expected := []string{"bar", "baz", "foo", "qux"}
assert.Equal(t, expected, sortStringsAsc(strings))
}
func TestSortStringsDesc(t *testing.T) {
strings := []string{"foo", "bar", "baz", "qux"}
expected := []string{"qux", "foo", "baz", "bar"}
assert.Equal(t, expected, sortStringsDesc(strings))
}
func TestSortObjectsByKeys(t *testing.T) {
o0 := &context.RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "bar.localhost",
},
ID: "9",
}
o1 := &context.RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "foo.localhost",
},
ID: "1",
}
o2 := &context.RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "baz.localhost",
},
ID: "3",
}
o3 := &context.RuntimeContainer{
Env: map[string]string{},
ID: "8",
}
containers := []*context.RuntimeContainer{o0, o1, o2, o3}
for _, tc := range []struct {
desc string
fn func(interface{}, string) ([]interface{}, error)
key string
want []interface{}
}{
{"Asc simple", sortObjectsByKeysAsc, "ID", []interface{}{o1, o2, o3, o0}},
{"Asc complex", sortObjectsByKeysAsc, "Env.VIRTUAL_HOST", []interface{}{o3, o0, o2, o1}},
{"Desc simple", sortObjectsByKeysDesc, "ID", []interface{}{o0, o3, o2, o1}},
{"Desc complex", sortObjectsByKeysDesc, "Env.VIRTUAL_HOST", []interface{}{o1, o2, o0, o3}},
} {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.fn(containers, tc.key)
assert.NoError(t, err)
// The function should return a sorted copy of the slice, not modify the original.
assert.Equal(t, []*context.RuntimeContainer{o0, o1, o2, o3}, containers)
assert.Equal(t, tc.want, got)
})
}
}

View File

@ -1,254 +0,0 @@
package template
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"syscall"
"text/template"
"unicode"
sprig "github.com/Masterminds/sprig/v3"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
func read(path string) (string, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
b, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(b), nil
}
func getArrayValues(funcName string, entries interface{}) (*reflect.Value, error) {
entriesVal := reflect.ValueOf(entries)
kind := entriesVal.Kind()
if kind == reflect.Ptr {
entriesVal = entriesVal.Elem()
kind = entriesVal.Kind()
}
switch kind {
case reflect.Array, reflect.Slice:
break
default:
return nil, fmt.Errorf("must pass an array or slice to '%v'; received %v; kind %v", funcName, entries, kind)
}
return &entriesVal, nil
}
func newTemplate(name string) *template.Template {
tmpl := template.New(name)
// The eval function is defined here because it must be a closure around tmpl.
eval := func(name string, args ...any) (string, error) {
buf := bytes.NewBuffer(nil)
data := any(nil)
if len(args) == 1 {
data = args[0]
} else if len(args) > 1 {
return "", errors.New("too many arguments")
}
if err := tmpl.ExecuteTemplate(buf, name, data); err != nil {
return "", err
}
return buf.String(), nil
}
tmpl.Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"coalesceempty": coalesceempty,
"contains": contains,
"dir": dirList,
"eval": eval,
"exists": utils.PathExists,
"read": read,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"json": marshalJson,
"intersect": intersect,
"keys": keys,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"sortStringsAsc": sortStringsAsc,
"sortStringsDesc": sortStringsDesc,
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
func isBlank(str string) bool {
for _, r := range str {
if !unicode.IsSpace(r) {
return false
}
}
return true
}
func removeBlankLines(reader io.Reader, writer io.Writer) {
breader := bufio.NewReader(reader)
bwriter := bufio.NewWriter(writer)
for {
line, err := breader.ReadString('\n')
if !isBlank(line) {
bwriter.WriteString(line)
}
if err != nil {
break
}
}
bwriter.Flush()
}
func filterRunning(config config.Config, containers context.Context) context.Context {
if config.IncludeStopped {
return containers
} else {
filteredContainers := context.Context{}
for _, container := range containers {
if container.State.Running {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers
}
}
func GenerateFile(config config.Config, containers context.Context) bool {
filteredRunningContainers := filterRunning(config, containers)
filteredContainers := context.Context{}
if config.OnlyPublished {
for _, container := range filteredRunningContainers {
if len(container.PublishedAddresses()) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else if config.OnlyExposed {
for _, container := range filteredRunningContainers {
if len(container.Addresses) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else {
filteredContainers = filteredRunningContainers
}
contents := executeTemplate(config.Template, filteredContainers)
if !config.KeepBlankLines {
buf := new(bytes.Buffer)
removeBlankLines(bytes.NewReader(contents), buf)
contents = buf.Bytes()
}
if config.Dest != "" {
dest, err := os.CreateTemp(filepath.Dir(config.Dest), "docker-gen")
defer func() {
dest.Close()
os.Remove(dest.Name())
}()
if err != nil {
log.Fatalf("Unable to create temp file: %s\n", err)
}
if n, err := dest.Write(contents); n != len(contents) || err != nil {
log.Fatalf("Failed to write to temp file: wrote %d, exp %d, err=%v", n, len(contents), err)
}
oldContents := []byte{}
if fi, err := os.Stat(config.Dest); err == nil || os.IsNotExist(err) {
if err != nil && os.IsNotExist(err) {
emptyFile, err := os.Create(config.Dest)
if err != nil {
log.Fatalf("Unable to create empty destination file: %s\n", err)
} else {
emptyFile.Close()
fi, _ = os.Stat(config.Dest)
}
}
if err := dest.Chmod(fi.Mode()); err != nil {
log.Fatalf("Unable to chmod temp file: %s\n", err)
}
if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil {
log.Fatalf("Unable to chown temp file: %s\n", err)
}
oldContents, err = os.ReadFile(config.Dest)
if err != nil {
log.Fatalf("Unable to compare current file contents: %s: %s\n", config.Dest, err)
}
}
if !bytes.Equal(oldContents, contents) {
err = os.Rename(dest.Name(), config.Dest)
if err != nil {
log.Fatalf("Unable to create dest file %s: %s\n", config.Dest, err)
}
log.Printf("Generated '%s' from %d containers", config.Dest, len(filteredContainers))
return true
}
return false
} else {
os.Stdout.Write(contents)
}
return true
}
func executeTemplate(templatePath string, containers context.Context) []byte {
tmpl, err := newTemplate(filepath.Base(templatePath)).ParseFiles(templatePath)
if err != nil {
log.Fatalf("Unable to parse template: %s", err)
}
buf := new(bytes.Buffer)
err = tmpl.ExecuteTemplate(buf, filepath.Base(templatePath), &containers)
if err != nil {
log.Fatalf("Template error: %s\n", err)
}
return buf.Bytes()
}

View File

@ -1,195 +0,0 @@
package template
import (
"bytes"
"errors"
"reflect"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type templateTestList []struct {
tmpl string
context interface{}
expected interface{}
}
func (tests templateTestList) run(t *testing.T) {
for n, test := range tests {
test := test
t.Run(strconv.Itoa(n), func(t *testing.T) {
t.Parallel()
wantErr, _ := test.expected.(error)
want, ok := test.expected.(string)
if !ok && wantErr == nil {
t.Fatalf("test bug: want a string or error for .expected, got %v", test.expected)
}
tmpl, err := newTemplate("testTemplate").Parse(test.tmpl)
if err != nil {
t.Fatalf("Template parse failed: %v", err)
}
var b bytes.Buffer
err = tmpl.ExecuteTemplate(&b, "testTemplate", test.context)
got := b.String()
if err != nil {
if wantErr != nil {
return
}
t.Fatalf("Error executing template: %v", err)
} else if wantErr != nil {
t.Fatalf("Expected error, got %v", got)
}
if want != got {
t.Fatalf("Incorrect output found; want %#v, got %#v", want, got)
}
})
}
}
func TestGetArrayValues(t *testing.T) {
values := []string{"foor", "bar", "baz"}
var expectedType *reflect.Value
arrayValues, err := getArrayValues("testFunc", values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "bar", arrayValues.Index(1).String())
arrayValues, err = getArrayValues("testFunc", &values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "baz", arrayValues.Index(2).String())
arrayValues, err = getArrayValues("testFunc", "foo")
assert.Error(t, err)
assert.Nil(t, arrayValues)
}
func TestIsBlank(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"", true},
{" ", true},
{" ", true},
{"\t", true},
{"\t\n\v\f\r\u0085\u00A0", true},
{"a", false},
{" a ", false},
{"a ", false},
{" a", false},
{"日本語", false},
}
for _, i := range tests {
v := isBlank(i.input)
if v != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, v)
}
}
}
func TestRemoveBlankLines(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", ""},
{"\r\n\r\n", ""},
{"line1\nline2", "line1\nline2"},
{"line1\n\nline2", "line1\nline2"},
{"\n\n\n\nline1\n\nline2", "line1\nline2"},
{"\n\n\n\n\n \n \n \n", ""},
// windows line endings \r\n
{"line1\r\nline2", "line1\r\nline2"},
{"line1\r\n\r\nline2", "line1\r\nline2"},
// keep last new line
{"line1\n", "line1\n"},
{"line1\r\n", "line1\r\n"},
}
for _, i := range tests {
output := new(bytes.Buffer)
removeBlankLines(strings.NewReader(i.input), output)
if output.String() != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, output)
}
}
}
// TestSprig ensures that the migration to sprig to provide certain functions did not break
// compatibility with existing templates.
func TestSprig(t *testing.T) {
for _, tc := range []struct {
desc string
tts templateTestList
}{
{"dict", templateTestList{
{`{{ $d := dict "a" "b" }}{{ if eq (index $d "a") "b" }}ok{{ end }}`, nil, `ok`},
{`{{ $d := dict "a" "b" }}{{ if eq (index $d "x") nil }}ok{{ end }}`, nil, `ok`},
{`{{ $d := dict "a" "b" "c" (dict "d" "e") }}{{ if eq (index $d "c" "d") "e" }}ok{{ end }}`, nil, `ok`},
}},
{"first", templateTestList{
{`{{ if eq (first $) "a"}}ok{{ end }}`, []string{"a", "b"}, `ok`},
{`{{ if eq (first $) "a"}}ok{{ end }}`, [2]string{"a", "b"}, `ok`},
}},
{"hasPrefix", templateTestList{
{`{{ if hasPrefix "tcp://" "tcp://127.0.0.1:2375" }}ok{{ end }}`, nil, `ok`},
{`{{ if not (hasPrefix "udp://" "tcp://127.0.0.1:2375") }}ok{{ end }}`, nil, `ok`},
}},
{"hasSuffix", templateTestList{
{`{{ if hasSuffix ".local" "myhost.local" }}ok{{ end }}`, nil, `ok`},
{`{{ if not (hasSuffix ".example" "myhost.local") }}ok{{ end }}`, nil, `ok`},
}},
{"last", templateTestList{
{`{{ if eq (last $) "b"}}ok{{ end }}`, []string{"a", "b"}, `ok`},
{`{{ if eq (last $) "b"}}ok{{ end }}`, [2]string{"a", "b"}, `ok`},
}},
{"trim", templateTestList{
{`{{ if eq (trim " myhost.local ") "myhost.local" }}ok{{ end }}`, nil, `ok`},
}},
} {
t.Run(tc.desc, func(t *testing.T) {
tc.tts.run(t)
})
}
}
func TestEval(t *testing.T) {
for _, tc := range []struct {
desc string
tts templateTestList
}{
{"undefined", templateTestList{
{`{{eval "missing"}}`, nil, errors.New("")},
{`{{eval "missing" nil}}`, nil, errors.New("")},
{`{{eval "missing" "abc"}}`, nil, errors.New("")},
{`{{eval "missing" "abc" "def"}}`, nil, errors.New("")},
}},
// The purpose of the "ctx" context is to assert that $ and . inside the template is the
// eval argument, not the global context.
{"noArg", templateTestList{
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T"}}`, "ctx", "<no value><no value>"},
}},
{"nilArg", templateTestList{
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" nil}}`, "ctx", "<no value><no value>"},
}},
{"oneArg", templateTestList{
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" "arg"}}`, "ctx", "argarg"},
}},
{"moreThanOneArg", templateTestList{
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" "a" "b"}}`, "ctx", errors.New("")},
}},
} {
t.Run(tc.desc, func(t *testing.T) {
tc.tts.run(t)
})
}
}

View File

@ -1,125 +0,0 @@
package template
import (
"reflect"
"regexp"
"strings"
"github.com/nginx-proxy/docker-gen/internal/context"
)
// Generalized where function
func generalizedWhere(funcName string, entries interface{}, key string, test func(interface{}) bool) (interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
selection := make([]interface{}, 0)
for i := 0; i < entriesVal.Len(); i++ {
v := entriesVal.Index(i).Interface()
value := deepGet(v, key)
if test(value) {
selection = append(selection, v)
}
}
return selection, nil
}
// selects entries based on key
func where(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("where", entries, key, func(value interface{}) bool {
return reflect.DeepEqual(value, cmp)
})
}
// select entries where a key is not equal to a value
func whereNot(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("whereNot", entries, key, func(value interface{}) bool {
return !reflect.DeepEqual(value, cmp)
})
}
// selects entries where a key exists
func whereExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereExist", entries, key, func(value interface{}) bool {
return value != nil
})
}
// selects entries where a key does not exist
func whereNotExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereNotExist", entries, key, func(value interface{}) bool {
return value == nil
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAny(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
return generalizedWhere("whereAny", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) > 0
}
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAll(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
req_count := len(cmp)
return generalizedWhere("whereAll", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) == req_count
}
})
}
// generalized whereLabel function
func generalizedWhereLabel(funcName string, containers context.Context, label string, test func(string, bool) bool) (context.Context, error) {
selection := make([]*context.RuntimeContainer, 0)
for i := 0; i < len(containers); i++ {
container := containers[i]
value, ok := container.Labels[label]
if test(value, ok) {
selection = append(selection, container)
}
}
return selection, nil
}
// selects containers that have a particular label
func whereLabelExists(containers context.Context, label string) (context.Context, error) {
return generalizedWhereLabel("whereLabelExists", containers, label, func(_ string, ok bool) bool {
return ok
})
}
// selects containers that have don't have a particular label
func whereLabelDoesNotExist(containers context.Context, label string) (context.Context, error) {
return generalizedWhereLabel("whereLabelDoesNotExist", containers, label, func(_ string, ok bool) bool {
return !ok
})
}
// selects containers with a particular label whose value matches a regular expression
func whereLabelValueMatches(containers context.Context, label, pattern string) (context.Context, error) {
rx, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return generalizedWhereLabel("whereLabelValueMatches", containers, label, func(value string, ok bool) bool {
return ok && rx.MatchString(value)
})
}

View File

@ -1,374 +0,0 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
)
func TestWhere(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{where . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `1`},
{`{{where . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{where . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `1`},
{`{{where . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `0`},
{`{{where .Addresses "Port" "80" | len}}`, containers[0], `1`},
{`{{where .Addresses "Port" "80" | len}}`, containers[1], `0`},
{
`{{where . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`2`,
},
}
tests.run(t)
}
func TestWhereNot(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNot . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `4`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[0], `0`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[1], `1`},
{
`{{whereNot . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`1`,
},
}
tests.run(t)
}
func TestWhereExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereExist . "Env.VIRTUAL_HOST" | len}}`, containers, `3`},
{`{{whereExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereExist . "Env.NOT_A_KEY" | len}}`, containers, `0`},
{`{{whereExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `1`},
}
tests.run(t)
}
func TestWhereNotExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNotExist . "Env.VIRTUAL_HOST" | len}}`, containers, `1`},
{`{{whereNotExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereNotExist . "Env.NOT_A_KEY" | len}}`, containers, `4`},
{`{{whereNotExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `3`},
}
tests.run(t)
}
func TestWhereSomeMatch(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `2`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "something,demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t)
}
func TestWhereRequires(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `0`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t)
}
func TestWhereLabelExists(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelExists . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelExists . "com.example.bar" | len}}`, containers, `2`},
{`{{whereLabelExists . "com.example.baz" | len}}`, containers, `0`},
}
tests.run(t)
}
func TestWhereLabelDoesNotExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelDoesNotExist . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelDoesNotExist . "com.example.bar" | len}}`, containers, `0`},
{`{{whereLabelDoesNotExist . "com.example.baz" | len}}`, containers, `2`},
}
tests.run(t)
}
func TestWhereLabelValueMatches(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "BAR",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelValueMatches . "com.example.foo" "^foo$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.foo" "\\d+" | len}}`, containers, `0`},
{`{{whereLabelValueMatches . "com.example.bar" "^bar$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.bar" "^(?i)bar$" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.bar" ".*" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.baz" ".*" | len}}`, containers, `0`},
}
tests.run(t)
}

View File

@ -1,34 +0,0 @@
package utils
import (
"os"
"strings"
)
// SplitKeyValueSlice takes a string slice where values are of the form
// KEY, KEY=, KEY=VALUE or KEY=NESTED_KEY=VALUE2, and returns a map[string]string where items
// are split at their first `=`.
func SplitKeyValueSlice(in []string) map[string]string {
env := make(map[string]string)
for _, entry := range in {
parts := strings.SplitN(entry, "=", 2)
if len(parts) != 2 {
parts = append(parts, "")
}
env[parts[0]] = parts[1]
}
return env
}
// PathExists returns whether the given file or directory exists or not
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

View File

@ -1,44 +0,0 @@
package utils
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitKeyValueSlice(t *testing.T) {
tests := []struct {
input []string
expected string
}{
{[]string{"K"}, ""},
{[]string{"K="}, ""},
{[]string{"K=V3"}, "V3"},
{[]string{"K=V4=V5"}, "V4=V5"},
}
for _, i := range tests {
v := SplitKeyValueSlice(i.input)
if v["K"] != i.expected {
t.Fatalf("expected K='%s'. got '%s'", i.expected, v["K"])
}
}
}
func TestPathExists(t *testing.T) {
file, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
exists, err := PathExists(file.Name())
assert.NoError(t, err)
assert.True(t, exists)
exists, err = PathExists("/wrong/path")
assert.NoError(t, err)
assert.False(t, exists)
}

49
reflect.go Normal file
View File

@ -0,0 +1,49 @@
package dockergen
import (
"log"
"reflect"
"strings"
)
func stripPrefix(s, prefix string) string {
path := s
for {
if strings.HasPrefix(path, ".") {
path = path[1:]
continue
}
break
}
return path
}
func deepGet(item interface{}, path string) interface{} {
if path == "" {
return item
}
path = stripPrefix(path, ".")
parts := strings.Split(path, ".")
itemValue := reflect.ValueOf(item)
if len(parts) > 0 {
switch itemValue.Kind() {
case reflect.Struct:
fieldValue := itemValue.FieldByName(parts[0])
if fieldValue.IsValid() {
return deepGet(fieldValue.Interface(), strings.Join(parts[1:], "."))
}
case reflect.Map:
mapValue := itemValue.MapIndex(reflect.ValueOf(parts[0]))
if mapValue.IsValid() {
return deepGet(mapValue.Interface(), strings.Join(parts[1:], "."))
}
default:
log.Printf("can't group by %s (value %v, kind %s)\n", path, itemValue, itemValue.Kind())
}
return nil
}
return itemValue.Interface()
}

61
reflect_test.go Normal file
View File

@ -0,0 +1,61 @@
package dockergen
import "testing"
func TestDeepGetNoPath(t *testing.T) {
item := RuntimeContainer{}
value := deepGet(item, "")
if _, ok := value.(RuntimeContainer); !ok {
t.Fail()
}
var returned RuntimeContainer
returned = value.(RuntimeContainer)
if !returned.Equals(item) {
t.Fail()
}
}
func TestDeepGetSimple(t *testing.T) {
item := RuntimeContainer{
ID: "expected",
}
value := deepGet(item, "ID")
if _, ok := value.(string); !ok {
t.Errorf("expected: %#v. got: %#v", "expected", value)
}
if value != "expected" {
t.Errorf("expected: %s. got: %s", "expected", value)
}
}
func TestDeepGetSimpleDotPrefix(t *testing.T) {
item := RuntimeContainer{
ID: "expected",
}
value := deepGet(item, "...ID")
if _, ok := value.(string); !ok {
t.Errorf("expected: %#v. got: %#v", "expected", value)
}
if value != "expected" {
t.Errorf("expected: %s. got: %s", "expected", value)
}
}
func TestDeepGetMap(t *testing.T) {
item := RuntimeContainer{
Env: map[string]string{
"key": "value",
},
}
value := deepGet(item, "Env.key")
if _, ok := value.(string); !ok {
t.Errorf("expected: %#v. got: %#v", "value", value)
}
if value != "value" {
t.Errorf("expected: %s. got: %s", "value", value)
}
}

509
template.go Normal file
View File

@ -0,0 +1,509 @@
package dockergen
import (
"bytes"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
"text/template"
)
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func getArrayValues(funcName string, entries interface{}) (*reflect.Value, error) {
entriesVal := reflect.ValueOf(entries)
kind := entriesVal.Kind()
if kind == reflect.Ptr {
entriesVal = reflect.Indirect(entriesVal)
kind = entriesVal.Kind()
}
switch kind {
case reflect.Array, reflect.Slice:
break
default:
return nil, fmt.Errorf("Must pass an array or slice to '%v'; received %v; kind %v", funcName, entries, kind)
}
return &entriesVal, nil
}
// Generalized groupBy function
func generalizedGroupBy(funcName string, entries interface{}, key string, addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
groups := make(map[string][]interface{})
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value := deepGet(v, key)
if value != nil {
addEntry(groups, value, v)
}
}
return groups, nil
}
func groupByMulti(entries interface{}, key, sep string) (map[string][]interface{}, error) {
return generalizedGroupBy("groupByMulti", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
items := strings.Split(value.(string), sep)
for _, item := range items {
groups[item] = append(groups[item], v)
}
})
}
// groupBy groups a generic array or slice by the path property key
func groupBy(entries interface{}, key string) (map[string][]interface{}, error) {
return generalizedGroupBy("groupBy", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
// groupByKeys is the same as groupBy but only returns a list of keys
func groupByKeys(entries interface{}, key string) ([]string, error) {
keys, err := generalizedGroupBy("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
if err != nil {
return nil, err
}
ret := []string{}
for k := range keys {
ret = append(ret, k)
}
return ret, nil
}
// Generalized where function
func generalizedWhere(funcName string, entries interface{}, key string, test func(interface{}) bool) (interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
selection := make([]interface{}, 0)
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value := deepGet(v, key)
if test(value) {
selection = append(selection, v)
}
}
return selection, nil
}
// selects entries based on key
func where(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("where", entries, key, func(value interface{}) bool {
return reflect.DeepEqual(value, cmp)
})
}
// selects entries where a key exists
func whereExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereExist", entries, key, func(value interface{}) bool {
return value != nil
})
}
// selects entries where a key does not exist
func whereNotExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereNotExist", entries, key, func(value interface{}) bool {
return value == nil
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAny(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
return generalizedWhere("whereAny", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) > 0
}
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAll(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
req_count := len(cmp)
return generalizedWhere("whereAll", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) == req_count
}
})
}
// generalized whereLabel function
func generalizedWhereLabel(funcName string, containers Context, label string, test func(string, bool) bool) (Context, error) {
selection := make([]*RuntimeContainer, 0)
for i := 0; i < len(containers); i++ {
container := containers[i]
value, ok := container.Labels[label]
if test(value, ok) {
selection = append(selection, container)
}
}
return selection, nil
}
// selects containers that have a particular label
func whereLabelExists(containers Context, label string) (Context, error) {
return generalizedWhereLabel("whereLabelExists", containers, label, func(_ string, ok bool) bool {
return ok
})
}
// selects containers that have don't have a particular label
func whereLabelDoesNotExist(containers Context, label string) (Context, error) {
return generalizedWhereLabel("whereLabelDoesNotExist", containers, label, func(_ string, ok bool) bool {
return !ok
})
}
// selects containers with a particular label whose value matches a regular expression
func whereLabelValueMatches(containers Context, label, pattern string) (Context, error) {
rx, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return generalizedWhereLabel("whereLabelValueMatches", containers, label, func(value string, ok bool) bool {
return ok && rx.MatchString(value)
})
}
// hasPrefix returns whether a given string is a prefix of another string
func hasPrefix(prefix, s string) bool {
return strings.HasPrefix(s, prefix)
}
// hasSuffix returns whether a given string is a suffix of another string
func hasSuffix(suffix, s string) bool {
return strings.HasSuffix(s, suffix)
}
func keys(input interface{}) (interface{}, error) {
if input == nil {
return nil, nil
}
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, fmt.Errorf("Cannot call keys on a non-map value: %v", input)
}
vk := val.MapKeys()
k := make([]interface{}, val.Len())
for i, _ := range k {
k[i] = vk[i].Interface()
}
return k, nil
}
func intersect(l1, l2 []string) []string {
m := make(map[string]bool)
m2 := make(map[string]bool)
for _, v := range l2 {
m2[v] = true
}
for _, v := range l1 {
if m2[v] {
m[v] = true
}
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func contains(item map[string]string, key string) bool {
if _, ok := item[key]; ok {
return true
}
return false
}
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}
func hashSha1(input string) string {
h := sha1.New()
io.WriteString(h, input)
return fmt.Sprintf("%x", h.Sum(nil))
}
func marshalJson(input interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(input); err != nil {
return "", err
}
return strings.TrimSuffix(buf.String(), "\n"), nil
}
func unmarshalJson(input string) (interface{}, error) {
var v interface{}
if err := json.Unmarshal([]byte(input), &v); err != nil {
return nil, err
}
return v, nil
}
// arrayFirst returns first item in the array or nil if the
// input is nil or empty
func arrayFirst(input interface{}) interface{} {
if input == nil {
return nil
}
arr := reflect.ValueOf(input)
if arr.Len() == 0 {
return nil
}
return arr.Index(0).Interface()
}
// arrayLast returns last item in the array
func arrayLast(input interface{}) interface{} {
arr := reflect.ValueOf(input)
return arr.Index(arr.Len() - 1).Interface()
}
// arrayClosest find the longest matching substring in values
// that matches input
func arrayClosest(values []string, input string) string {
best := ""
for _, v := range values {
if strings.Contains(input, v) && len(v) > len(best) {
best = v
}
}
return best
}
// dirList returns a list of files in the specified path
func dirList(path string) ([]string, error) {
names := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
return names, err
}
for _, f := range files {
names = append(names, f.Name())
}
return names, nil
}
// coalesce returns the first non nil argument
func coalesce(input ...interface{}) interface{} {
for _, v := range input {
if v != nil {
return v
}
}
return nil
}
// trimPrefix returns a string without the prefix, if present
func trimPrefix(prefix, s string) string {
return strings.TrimPrefix(s, prefix)
}
// trimSuffix returns a string without the suffix, if present
func trimSuffix(suffix, s string) string {
return strings.TrimSuffix(s, suffix)
}
// trim returns the string without leading or trailing whitespace
func trim(s string) string {
return strings.TrimSpace(s)
}
// when returns the trueValue when the condition is true and the falseValue otherwise
func when(condition bool, trueValue, falseValue interface{}) interface{} {
if condition {
return trueValue
} else {
return falseValue
}
}
func newTemplate(name string) *template.Template {
tmpl := template.New(name).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dict": dict,
"dir": dirList,
"exists": exists,
"first": arrayFirst,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"hasPrefix": hasPrefix,
"hasSuffix": hasSuffix,
"json": marshalJson,
"intersect": intersect,
"keys": keys,
"last": arrayLast,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"trim": trim,
"when": when,
"where": where,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
func GenerateFile(config Config, containers Context) bool {
filteredContainers := Context{}
if config.OnlyPublished {
for _, container := range containers {
if len(container.PublishedAddresses()) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else if config.OnlyExposed {
for _, container := range containers {
if len(container.Addresses) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else {
filteredContainers = containers
}
contents := executeTemplate(config.Template, filteredContainers)
if !config.KeepBlankLines {
buf := new(bytes.Buffer)
removeBlankLines(bytes.NewReader(contents), buf)
contents = buf.Bytes()
}
if config.Dest != "" {
dest, err := ioutil.TempFile(filepath.Dir(config.Dest), "docker-gen")
defer func() {
dest.Close()
os.Remove(dest.Name())
}()
if err != nil {
log.Fatalf("unable to create temp file: %s\n", err)
}
if n, err := dest.Write(contents); n != len(contents) || err != nil {
log.Fatalf("failed to write to temp file: wrote %d, exp %d, err=%v", n, len(contents), err)
}
oldContents := []byte{}
if fi, err := os.Stat(config.Dest); err == nil {
if err := dest.Chmod(fi.Mode()); err != nil {
log.Fatalf("unable to chmod temp file: %s\n", err)
}
if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil {
log.Fatalf("unable to chown temp file: %s\n", err)
}
oldContents, err = ioutil.ReadFile(config.Dest)
if err != nil {
log.Fatalf("unable to compare current file contents: %s: %s\n", config.Dest, err)
}
}
if bytes.Compare(oldContents, contents) != 0 {
err = os.Rename(dest.Name(), config.Dest)
if err != nil {
log.Fatalf("unable to create dest file %s: %s\n", config.Dest, err)
}
log.Printf("Generated '%s' from %d containers", config.Dest, len(filteredContainers))
return true
}
return false
} else {
os.Stdout.Write(contents)
}
return true
}
func executeTemplate(templatePath string, containers Context) []byte {
tmpl, err := newTemplate(filepath.Base(templatePath)).ParseFiles(templatePath)
if err != nil {
log.Fatalf("unable to parse template: %s", err)
}
buf := new(bytes.Buffer)
err = tmpl.ExecuteTemplate(buf, filepath.Base(templatePath), &containers)
if err != nil {
log.Fatalf("template error: %s\n", err)
}
return buf.Bytes()
}

745
template_test.go Normal file
View File

@ -0,0 +1,745 @@
package dockergen
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"text/template"
)
type templateTestList []struct {
tmpl string
context interface{}
expected string
}
func (tests templateTestList) run(t *testing.T, prefix string) {
for n, test := range tests {
tmplName := fmt.Sprintf("%s-test-%d", prefix, n)
tmpl := template.Must(newTemplate(tmplName).Parse(test.tmpl))
var b bytes.Buffer
err := tmpl.ExecuteTemplate(&b, tmplName, test.context)
if err != nil {
t.Fatalf("Error executing template: %v (test %s)", err, tmplName)
}
got := b.String()
if test.expected != got {
t.Fatalf("Incorrect output found; expected %s, got %s (test %s)", test.expected, got, tmplName)
}
}
}
func TestContains(t *testing.T) {
env := map[string]string{
"PORT": "1234",
}
if !contains(env, "PORT") {
t.Fail()
}
if contains(env, "MISSING") {
t.Fail()
}
}
func TestKeys(t *testing.T) {
env := map[string]string{
"VIRTUAL_HOST": "demo.local",
}
tests := templateTestList{
{`{{range (keys $)}}{{.}}{{end}}`, env, `VIRTUAL_HOST`},
}
tests.run(t, "keys")
}
func TestKeysEmpty(t *testing.T) {
input := map[string]int{}
k, err := keys(input)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() == reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
if len(input) != vk.Len() {
t.Fatalf("Incorrect key count; expected %s, got %s", len(input), vk.Len())
}
}
func TestKeysNil(t *testing.T) {
k, err := keys(nil)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() != reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
}
func TestIntersect(t *testing.T) {
if len(intersect([]string{"foo.fo.com", "bar.com"}, []string{"foo.bar.com"})) != 0 {
t.Fatal("Expected no match")
}
if len(intersect([]string{"foo.fo.com", "bar.com"}, []string{"bar.com", "foo.com"})) != 1 {
t.Fatal("Expected only one match")
}
if len(intersect([]string{"foo.com"}, []string{"bar.com", "foo.com"})) != 1 {
t.Fatal("Expected only one match")
}
if len(intersect([]string{"foo.fo.com", "foo.com", "bar.com"}, []string{"bar.com", "foo.com"})) != 2 {
t.Fatal("Expected two matches")
}
}
func TestGroupByExistingKey(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, _ := groupBy(containers, "Env.VIRTUAL_HOST")
if len(groups) != 2 {
t.Fail()
}
if len(groups["demo1.localhost"]) != 2 {
t.Fail()
}
if len(groups["demo2.localhost"]) != 1 {
t.FailNow()
}
if groups["demo2.localhost"][0].(RuntimeContainer).ID != "3" {
t.Fail()
}
}
func TestGroupByAfterWhere(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
}
filtered, _ := where(containers, "Env.EXTERNAL", "true")
groups, _ := groupBy(filtered, "Env.VIRTUAL_HOST")
if len(groups) != 2 {
t.Fail()
}
if len(groups["demo1.localhost"]) != 1 {
t.Fail()
}
if len(groups["demo2.localhost"]) != 1 {
t.FailNow()
}
if groups["demo2.localhost"][0].(RuntimeContainer).ID != "3" {
t.Fail()
}
}
func TestGroupByMulti(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, _ := groupByMulti(containers, "Env.VIRTUAL_HOST", ",")
if len(groups) != 3 {
t.Fatalf("expected 3 got %d", len(groups))
}
if len(groups["demo1.localhost"]) != 2 {
t.Fatalf("expected 2 got %s", len(groups["demo1.localhost"]))
}
if len(groups["demo2.localhost"]) != 1 {
t.Fatalf("expected 1 got %s", len(groups["demo2.localhost"]))
}
if groups["demo2.localhost"][0].(RuntimeContainer).ID != "3" {
t.Fatalf("expected 2 got %s", groups["demo2.localhost"][0].(RuntimeContainer).ID)
}
if len(groups["demo3.localhost"]) != 1 {
t.Fatalf("expect 1 got %d", len(groups["demo3.localhost"]))
}
if groups["demo3.localhost"][0].(RuntimeContainer).ID != "2" {
t.Fatalf("expected 2 got %s", groups["demo3.localhost"][0].(RuntimeContainer).ID)
}
}
func TestWhere(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []Address{
Address{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []Address{
Address{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{where . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `1`},
{`{{where . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{where . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `1`},
{`{{where . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `0`},
{`{{where .Addresses "Port" "80" | len}}`, containers[0], `1`},
{`{{where .Addresses "Port" "80" | len}}`, containers[1], `0`},
{
`{{where . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`2`,
},
}
tests.run(t, "where")
}
func TestWhereExist(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereExist . "Env.VIRTUAL_HOST" | len}}`, containers, `3`},
{`{{whereExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereExist . "Env.NOT_A_KEY" | len}}`, containers, `0`},
{`{{whereExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `1`},
}
tests.run(t, "whereExist")
}
func TestWhereNotExist(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNotExist . "Env.VIRTUAL_HOST" | len}}`, containers, `1`},
{`{{whereNotExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereNotExist . "Env.NOT_A_KEY" | len}}`, containers, `4`},
{`{{whereNotExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `3`},
}
tests.run(t, "whereNotExist")
}
func TestWhereSomeMatch(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `2`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "something,demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAny")
}
func TestWhereRequires(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `0`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAll")
}
func TestWhereLabelExists(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
&RuntimeContainer{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelExists . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelExists . "com.example.bar" | len}}`, containers, `2`},
{`{{whereLabelExists . "com.example.baz" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelExists")
}
func TestWhereLabelDoesNotExist(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
&RuntimeContainer{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelDoesNotExist . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelDoesNotExist . "com.example.bar" | len}}`, containers, `0`},
{`{{whereLabelDoesNotExist . "com.example.baz" | len}}`, containers, `2`},
}
tests.run(t, "whereLabelDoesNotExist")
}
func TestWhereLabelValueMatches(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
&RuntimeContainer{
Labels: map[string]string{
"com.example.bar": "BAR",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelValueMatches . "com.example.foo" "^foo$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.foo" "\\d+" | len}}`, containers, `0`},
{`{{whereLabelValueMatches . "com.example.bar" "^bar$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.bar" "^(?i)bar$" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.bar" ".*" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.baz" ".*" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelValueMatches")
}
func TestHasPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
if !hasPrefix(prefix, str) {
t.Fatalf("expected %s to have prefix %s", str, prefix)
}
}
func TestHasSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
if !hasSuffix(suffix, str) {
t.Fatalf("expected %s to have suffix %s", str, suffix)
}
}
func TestSplitN(t *testing.T) {
tests := templateTestList{
{`{{index (splitN . "/" 2) 0}}`, "example.com/path", `example.com`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/path", `path`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/a/longer/path", `a/longer/path`},
{`{{len (splitN . "/" 2)}}`, "example.com", `1`},
}
tests.run(t, "splitN")
}
func TestTrimPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
const trimmed = "127.0.0.1:2375"
got := trimPrefix(prefix, str)
if got != trimmed {
t.Fatalf("expected trimPrefix(%s,%s) to be %s, got %s", prefix, str, trimmed, got)
}
}
func TestTrimSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
const trimmed = "myhost"
got := trimSuffix(suffix, str)
if got != trimmed {
t.Fatalf("expected trimSuffix(%s,%s) to be %s, got %s", suffix, str, trimmed, got)
}
}
func TestTrim(t *testing.T) {
const str = " myhost.local "
const trimmed = "myhost.local"
got := trim(str)
if got != trimmed {
t.Fatalf("expected trim(%s) to be %s, got %s", str, trimmed, got)
}
}
func TestDict(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
d, err := dict("/", containers)
if err != nil {
t.Fatal(err)
}
if d["/"] == nil {
t.Fatalf("did not find containers in dict: %s", d)
}
if d["MISSING"] != nil {
t.Fail()
}
}
func TestSha1(t *testing.T) {
sum := hashSha1("/path")
if sum != "4f26609ad3f5185faaa9edf1e93aa131e2131352" {
t.Fatal("Incorrect SHA1 sum")
}
}
func TestJson(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
&RuntimeContainer{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
output, err := marshalJson(containers)
if err != nil {
t.Fatal(err)
}
buf := bytes.NewBufferString(output)
dec := json.NewDecoder(buf)
if err != nil {
t.Fatal(err)
}
var decoded []*RuntimeContainer
if err := dec.Decode(&decoded); err != nil {
t.Fatal(err)
}
if len(decoded) != len(containers) {
t.Fatal("Incorrect unmarshaled container count. Expected %d, got %d.", len(containers), len(decoded))
}
}
func TestParseJson(t *testing.T) {
tests := templateTestList{
{`{{parseJson .}}`, `null`, `<no value>`},
{`{{parseJson .}}`, `true`, `true`},
{`{{parseJson .}}`, `1`, `1`},
{`{{parseJson .}}`, `0.5`, `0.5`},
{`{{index (parseJson .) "enabled"}}`, `{"enabled":true}`, `true`},
{`{{index (parseJson . | first) "enabled"}}`, `[{"enabled":true}]`, `true`},
}
tests.run(t, "parseJson")
}
func TestQueryEscape(t *testing.T) {
tests := templateTestList{
{`{{queryEscape .}}`, `example.com`, `example.com`},
{`{{queryEscape .}}`, `.example.com`, `.example.com`},
{`{{queryEscape .}}`, `*.example.com`, `%2A.example.com`},
{`{{queryEscape .}}`, `~^example\.com(\..*\.xip\.io)?$`, `~%5Eexample%5C.com%28%5C..%2A%5C.xip%5C.io%29%3F%24`},
}
tests.run(t, "queryEscape")
}
func TestArrayClosestExact(t *testing.T) {
if arrayClosest([]string{"foo.bar.com", "bar.com"}, "foo.bar.com") != "foo.bar.com" {
t.Fatal("Expected foo.bar.com")
}
}
func TestArrayClosestSubstring(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bar.com"}, "foo.bar.com") != "bar.com" {
t.Fatal("Expected bar.com")
}
}
func TestArrayClosestNoMatch(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bip.com"}, "foo.bar.com") != "" {
t.Fatal("Expected ''")
}
}
func TestWhen(t *testing.T) {
context := struct {
BoolValue bool
StringValue string
}{
true,
"foo",
}
tests := templateTestList{
{`{{ print (when .BoolValue "first" "second") }}`, context, `first`},
{`{{ print (when (eq .StringValue "foo") "first" "second") }}`, context, `first`},
{`{{ when (not .BoolValue) "first" "second" | print }}`, context, `second`},
{`{{ when (not (eq .StringValue "foo")) "first" "second" | print }}`, context, `second`},
}
tests.run(t, "when")
}
func TestWhenTrue(t *testing.T) {
if when(true, "first", "second") != "first" {
t.Fatal("Expected first value")
}
}
func TestWhenFalse(t *testing.T) {
if when(false, "first", "second") != "second" {
t.Fatal("Expected second value")
}
}

View File

@ -1,6 +1,3 @@
{{/* Simple dnsmasq template generating host entries */}}
{{/* Domains are hard-coded, replace 'docker.comany.com' below */}}
{{$domain := "docker.company.com"}}
{{range $key, $value := .}}
# {{ $value.Name }} ({{$value.ID}} from {{$value.Image.Repository}})

View File

@ -1,4 +1,3 @@
{{/* etcd template to generate registration script */}}
#!/bin/bash

View File

@ -1,4 +1,3 @@
{{/* Generates fluentd configuration entries */}}
## File input
## read docker logs with tag=docker.container

View File

@ -1,7 +1,3 @@
{{/* Generate logrotate snippets for logrotate based on files listed in */}}
{{/* the comma separated environment variable LOG_FILES */}}
{{/* e.g. docker run --env='/var/log/messages,/var/log/lastlog' ... */}}
{{ range $index, $value := $ }}
{{ $logs := $value.Env.LOG_FILES }}
{{ if $logs }}

View File

@ -1,8 +1,3 @@
{{/* default nginx configuration template */}}
{{/* Generate a configuration file based on the containers mandatory */}}
{{/* VIRTUAL_HOST environment variable and the exposed ports. If multiple */}}
{{/* ports are exposed, the first one is used, unless set with VIRTUAL_PORT */}}
server {
listen 80 default_server;
server_name _; # This is just an invalid value which will never trigger on a real hostname.
@ -17,22 +12,20 @@ upstream {{ $host }} {
{{ range $index, $value := $containers }}
{{ $addrLen := len $value.Addresses }}
{{ $network := index $value.Networks 0 }}
{{/* If only 1 port exposed, use that */}}
{{ if eq $addrLen 1 }}
{{ with $address := index $value.Addresses 0 }}
# {{$value.Name}}
server {{ $network.IP }}:{{ $address.Port }};
# {{$value.Name}}
server {{ $address.IP }}:{{ $address.Port }};
{{ end }}
{{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var */}}
{{ else if $value.Env.VIRTUAL_PORT }}
{{ range $i, $address := $value.Addresses }}
{{ if eq $address.Port $value.Env.VIRTUAL_PORT }}
# {{$value.Name}}
server {{ $network.IP }}:{{ $address.Port }};
{{ end }}
{{ if eq $address.Port $value.Env.VIRTUAL_PORT }}
# {{$value.Name}}
server {{ $address.IP }}:{{ $address.Port }};
{{ end }}
{{ end }}
{{/* Else default to standard web port 80 */}}
@ -40,7 +33,7 @@ upstream {{ $host }} {
{{ range $i, $address := $value.Addresses }}
{{ if eq $address.Port "80" }}
# {{$value.Name}}
server {{ $network.IP }}:{{ $address.Port }};
server {{ $address.IP }}:{{ $address.Port }};
{{ end }}
{{ end }}
{{ end }}
@ -67,4 +60,4 @@ server {
proxy_set_header Connection "";
}
}
{{ end }}
{{ end }}

71
utils.go Normal file
View File

@ -0,0 +1,71 @@
package dockergen
import (
"bufio"
"io"
"os"
"strings"
"unicode"
)
func GetEndpoint(endpoint string) (string, error) {
defaultEndpoint := "unix:///var/run/docker.sock"
if os.Getenv("DOCKER_HOST") != "" {
defaultEndpoint = os.Getenv("DOCKER_HOST")
}
if endpoint != "" {
defaultEndpoint = endpoint
}
_, _, err := parseHost(defaultEndpoint)
if err != nil {
return "", err
}
return defaultEndpoint, nil
}
// splitKeyValueSlice takes a string slice where values are of the form
// KEY, KEY=, KEY=VALUE or KEY=NESTED_KEY=VALUE2, and returns a map[string]string where items
// are split at their first `=`.
func splitKeyValueSlice(in []string) map[string]string {
env := make(map[string]string)
for _, entry := range in {
parts := strings.SplitN(entry, "=", 2)
if len(parts) != 2 {
parts = append(parts, "")
}
env[parts[0]] = parts[1]
}
return env
}
func isBlank(str string) bool {
for _, r := range str {
if !unicode.IsSpace(r) {
return false
}
}
return true
}
func removeBlankLines(reader io.Reader, writer io.Writer) {
breader := bufio.NewReader(reader)
bwriter := bufio.NewWriter(writer)
for {
line, err := breader.ReadString('\n')
if !isBlank(line) {
bwriter.WriteString(line)
}
if err != nil {
break
}
}
bwriter.Flush()
}

139
utils_test.go Normal file
View File

@ -0,0 +1,139 @@
package dockergen
import (
"bytes"
"os"
"strings"
"testing"
)
func TestDefaultEndpoint(t *testing.T) {
err := os.Unsetenv("DOCKER_HOST")
if err != nil {
t.Fatalf("Unable to unset DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "unix:///var/run/docker.sock" {
t.Fatalf("Expected unix:///var/run/docker.sock, got %s", endpoint)
}
}
func TestDockerHostEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatal("%s", err)
}
if endpoint != "tcp://127.0.0.1:4243" {
t.Fatalf("Expected tcp://127.0.0.1:4243, got %s", endpoint)
}
}
func TestDockerFlagEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
// flag value should override DOCKER_HOST and default value
endpoint, err := GetEndpoint("tcp://127.0.0.1:5555")
if err != nil {
t.Fatal("%s", err)
}
if endpoint != "tcp://127.0.0.1:5555" {
t.Fatalf("Expected tcp://127.0.0.1:5555, got %s", endpoint)
}
}
func TestUnixBadFormat(t *testing.T) {
endpoint := "unix:/var/run/docker.sock"
_, err := GetEndpoint(endpoint)
if err == nil {
t.Fatal("endpoint should have failed")
}
}
func TestSplitKeyValueSlice(t *testing.T) {
tests := []struct {
input []string
expected string
}{
{[]string{"K"}, ""},
{[]string{"K="}, ""},
{[]string{"K=V3"}, "V3"},
{[]string{"K=V4=V5"}, "V4=V5"},
}
for _, i := range tests {
v := splitKeyValueSlice(i.input)
if v["K"] != i.expected {
t.Fatalf("expected K='%s'. got '%s'", i.expected, v["K"])
}
}
}
func TestIsBlank(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"", true},
{" ", true},
{" ", true},
{"\t", true},
{"\t\n\v\f\r\u0085\u00A0", true},
{"a", false},
{" a ", false},
{"a ", false},
{" a", false},
{"日本語", false},
}
for _, i := range tests {
v := isBlank(i.input)
if v != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, v)
}
}
}
func TestRemoveBlankLines(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", ""},
{"\r\n\r\n", ""},
{"line1\nline2", "line1\nline2"},
{"line1\n\nline2", "line1\nline2"},
{"\n\n\n\nline1\n\nline2", "line1\nline2"},
{"\n\n\n\n\n \n \n \n", ""},
// windows line endings \r\n
{"line1\r\nline2", "line1\r\nline2"},
{"line1\r\n\r\nline2", "line1\r\nline2"},
// keep last new line
{"line1\n", "line1\n"},
{"line1\r\n", "line1\r\n"},
}
for _, i := range tests {
output := new(bytes.Buffer)
removeBlankLines(strings.NewReader(i.input), output)
if output.String() != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, output)
}
}
}