Compare commits

..

1 Commits

Author SHA1 Message Date
nicolas.dorier
18ba39e768 Add read function to read the content of a file 2019-03-18 23:04:45 +09:00
54 changed files with 2272 additions and 3440 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

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: go
go:
- "1.9"
- "1.10"
- "1.11"
install:
- make get-deps
script:
- make all check-gofmt test

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM alpine:latest
LABEL maintainer="Jason Wilder <mail@jasonwilder.com>"
RUN apk -U add openssl
ENV VERSION 0.7.3
ENV DOWNLOAD_URL https://github.com/jwilder/docker-gen/releases/download/$VERSION/docker-gen-alpine-linux-amd64-$VERSION.tar.gz
ENV DOCKER_HOST unix:///tmp/docker.sock
RUN wget -qO- $DOWNLOAD_URL | tar xvz -C /usr/local/bin
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"]

6
GLOCKFILE Normal file
View File

@ -0,0 +1,6 @@
github.com/BurntSushi/toml 056c9bc7be7190eaa7715723883caffa5f8fa3e4
github.com/docker/docker f2afa26235941fd79f40eb1e572e19e4ac2b9bbe
github.com/docker/go-units 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
github.com/fsouza/go-dockerclient d2a6d0596004cc01062a2a068540b817f911e6dc
github.com/gorilla/mux d391bea3118c9fc17a88d62c9189bb791255e0ef
golang.org/x/net a04bdaca5b32abe1c069418fb7088ae607de5bd0

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,7 +17,7 @@ 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/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/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/linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/linux/amd64/docker-gen ./cmd/docker-gen
@ -26,10 +26,11 @@ dist: dist-clean
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
@ -39,21 +40,18 @@ release: dist
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

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.7.3](https://img.shields.io/badge/latest-0.7.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,17 @@ 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.7.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.7.3/docker-gen-linux-amd64-0.7.3.tar.gz)
* [i386](https://github.com/jwilder/docker-gen/releases/download/0.7.3/docker-gen-linux-i386-0.7.3.tar.gz)
* [alpine-linux](https://github.com/jwilder/docker-gen/releases/download/0.7.3/docker-gen-alpine-linux-amd64-0.7.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.7.3/docker-gen-linux-amd64-0.7.3.tar.gz
$ tar xvzf docker-gen-linux-amd64-0.7.3.tar.gz
$ ./docker-gen
```
@ -46,14 +43,14 @@ $ ./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)
It can also be run as two separate containers using the [jwilder/docker-gen](https://index.docker.io/u/jwilder/docker-gen/)
image, together with virtually any other image.
This is how you could run the official [nginx](https://registry.hub.docker.com/_/nginx/) image and
@ -69,17 +66,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,13 +95,8 @@ 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`
send HUP signal to container. Equivalent to 'docker kill -s HUP container-ID'
-only-exposed
only include containers with exposed ports
-only-published
@ -134,7 +120,7 @@ Options:
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
@ -157,7 +143,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)
@ -212,7 +198,7 @@ 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
@ -360,34 +346,32 @@ 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)
* *`read $string`*: Read the content of the file located at `$path`
* *`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.
@ -409,18 +393,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:
@ -451,11 +435,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,38 @@ 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
wait string
notifyCmd string
notifyOutput bool
notifySigHUPContainerID string
onlyExposed bool
onlyPublished bool
includeStopped 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 +68,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 {
@ -96,12 +95,8 @@ func initFlags() {
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", "",
flag.StringVar(&notifySigHUPContainerID, "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.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 +111,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 {
@ -140,29 +131,29 @@ func main() {
}
}
} else {
w, err := config.ParseWait(wait)
w, err := dockergen.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
@ -172,7 +163,7 @@ func main() {
}
}
generator, err := generator.NewGenerator(generator.GeneratorConfig{
generator, err := dockergen.NewGenerator(dockergen.GeneratorConfig{
Endpoint: endpoint,
TLSKey: tlsKey,
TLSCert: tlsCert,

View File

@ -1,9 +1,11 @@
package config
package dockergen
import (
"errors"
"strings"
"time"
"github.com/fsouza/go-dockerclient"
)
type Config struct {
@ -13,7 +15,7 @@ type Config struct {
Wait *Wait
NotifyCmd string
NotifyOutput bool
NotifyContainers map[string]int
NotifyContainers map[string]docker.Signal
OnlyExposed bool
OnlyPublished bool
IncludeStopped bool
@ -73,7 +75,7 @@ func ParseWait(s string) (*Wait, error) {
return nil, err
}
if max < min {
return nil, errors.New("invalid wait interval: max must be larger than min")
return nil, errors.New("Invalid wait interval: max must be larger than min")
}
} else {
max = 4 * min

View File

@ -1,14 +1,12 @@
package context
package dockergen
import (
"bufio"
"fmt"
"os"
"regexp"
"sync"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/utils"
"github.com/fsouza/go-dockerclient"
)
var (
@ -20,7 +18,7 @@ var (
type Context []*RuntimeContainer
func (c *Context) Env() map[string]string {
return utils.SplitKeyValueSlice(os.Environ())
return splitKeyValueSlice(os.Environ())
}
func (c *Context) Docker() Docker {
@ -160,78 +158,36 @@ type Docker struct {
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"}
func GetCurrentContainerID() string {
file, err := os.Open("/proc/self/cgroup")
if err != nil {
return ""
}
// 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
}
reader := bufio.NewReader(file)
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
_, lines, err := bufio.ScanLines([]byte(scanner.Text()), true)
if err == nil {
strLines := string(lines)
if id := matchDockerCurrentContainerID(strLines); id != "" {
return id
} else if id := matchECSCurrentContainerID(strLines); id != "" {
return id
}
}
}
// 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)
}
func matchDockerCurrentContainerID(lines string) string {
regex := "/docker[/-]([[:alnum:]]{64})(\\.scope)?$"
re := regexp.MustCompilePOSIX(regex)
re = regexp.MustCompilePOSIX(regex)
if re.MatchString(lines) {
submatches := re.FindStringSubmatch(string(lines))
containerID := submatches[1]
@ -240,3 +196,17 @@ func matchContainerID(regex, lines string) string {
}
return ""
}
func matchECSCurrentContainerID(lines string) string {
regex := "/ecs\\/[^\\/]+\\/(.+)$"
re := regexp.MustCompilePOSIX(regex)
if re.MatchString(string(lines)) {
submatches := re.FindStringSubmatch(string(lines))
containerID := submatches[1]
return containerID
}
return ""
}

52
context_test.go Normal file
View File

@ -0,0 +1,52 @@
package dockergen
import (
"testing"
)
func TestGetCurrentContainerID(t *testing.T) {
currentContainerID := GetCurrentContainerID()
if len(currentContainerID) != 0 && len(currentContainerID) != 64 {
t.Fail()
}
}
func TestGetCurrentContainerID_ECS(t *testing.T) {
cgroup :=
`9:perf_event:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
8:memory:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
7:hugetlb:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
6:freezer:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
5:devices:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
4:cpuset:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
3:cpuacct:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
2:cpu:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f
1:blkio:/ecs/628967a1-46b4-4a8a-84ff-605128f4679e/3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f`
if got, exp := matchECSCurrentContainerID(cgroup), "3c94e08259a6235781bb65f3dec91150c92e9d414ecc410d6245687392d3900f"; got != exp {
t.Fatalf("id mismatch: got %v, exp %v", got, exp)
}
}
func TestGetCurrentContainerID_DockerCE(t *testing.T) {
cgroup :=
`13:name=systemd:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
12:pids:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
11:hugetlb:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
10:net_prio:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
9:perf_event:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
8:net_cls:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
7:freezer:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
6:devices:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
5:memory:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
4:blkio:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
3:cpuacct:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
2:cpu:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb
1:cpuset:/docker-ce/docker/18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb`
if got, exp := matchDockerCurrentContainerID(cgroup), "18862cabc2e0d24142cf93c46ccb6e070c2ea7b996c81c0311ec0309abcbcdfb"; got != exp {
t.Fatalf("id mismatch: got %v, exp %v", got, exp)
}
}

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

@ -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

View File

@ -1,4 +1,4 @@
package generator
package dockergen
import (
"fmt"
@ -11,17 +11,12 @@ import (
"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"
"github.com/fsouza/go-dockerclient"
)
type generator struct {
Client *docker.Client
Configs config.ConfigFile
Configs ConfigFile
Endpoint string
TLSVerify bool
TLSCert, TLSCaCert, TLSKey string
@ -40,18 +35,18 @@ type GeneratorConfig struct {
TLSVerify bool
All bool
ConfigFile config.ConfigFile
ConfigFile ConfigFile
}
func NewGenerator(gc GeneratorConfig) (*generator, error) {
endpoint, err := dockerclient.GetEndpoint(gc.Endpoint)
endpoint, err := GetEndpoint(gc.Endpoint)
if err != nil {
return nil, fmt.Errorf("bad endpoint: %s", err)
return nil, fmt.Errorf("Bad endpoint: %s", err)
}
client, err := dockerclient.NewDockerClient(endpoint, gc.TLSVerify, gc.TLSCert, gc.TLSCACert, gc.TLSKey)
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)
return nil, fmt.Errorf("Unable to create docker client: %s", err)
}
apiVersion, err := client.Version()
@ -60,7 +55,7 @@ func NewGenerator(gc GeneratorConfig) (*generator, error) {
}
// Grab the docker daemon info once and hold onto it
context.SetDockerEnv(apiVersion)
SetDockerEnv(apiVersion)
return &generator{
Client: client,
@ -103,15 +98,14 @@ func (g *generator) generateFromSignals() {
go func() {
defer g.wg.Done()
sigChan, cleanup := newSignalChannel()
defer cleanup()
sigChan := newSignalChannel()
for {
sig := <-sigChan
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGHUP:
g.generateFromContainers()
case syscall.SIGTERM, syscall.SIGINT:
case syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT:
// exit when context is done
return
}
@ -126,7 +120,7 @@ func (g *generator) generateFromContainers() {
return
}
for _, config := range g.Configs.Config {
changed := template.GenerateFile(config, containers)
changed := GenerateFile(config, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
continue
@ -137,20 +131,19 @@ func (g *generator) generateFromContainers() {
}
func (g *generator) generateAtInterval() {
for _, cfg := range g.Configs.Config {
for _, config := range g.Configs.Config {
if cfg.Interval == 0 {
if config.Interval == 0 {
continue
}
log.Printf("Generating every %d seconds", cfg.Interval)
log.Printf("Generating every %d seconds", config.Interval)
g.wg.Add(1)
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
go func(cfg config.Config) {
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
go func(config Config) {
defer g.wg.Done()
sigChan, cleanup := newSignalChannel()
defer cleanup()
sigChan := newSignalChannel()
for {
select {
case <-ticker.C:
@ -160,19 +153,19 @@ func (g *generator) generateAtInterval() {
continue
}
// ignore changed return value. always run notify command
template.GenerateFile(cfg, containers)
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
GenerateFile(config, containers)
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
case sig := <-sigChan:
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGTERM, syscall.SIGINT:
case syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT:
ticker.Stop()
return
}
}
}
}(cfg)
}(config)
}
}
@ -185,55 +178,54 @@ func (g *generator) generateFromEvents() {
client := g.Client
var watchers []chan *docker.APIEvents
for _, cfg := range configs.Config {
for _, config := range configs.Config {
if !cfg.Watch {
if !config.Watch {
continue
}
g.wg.Add(1)
watcher := make(chan *docker.APIEvents, 100)
watchers = append(watchers, watcher)
go func(cfg config.Config) {
go func(config Config, watcher chan *docker.APIEvents) {
defer g.wg.Done()
debouncedChan := newDebounceChannel(watcher, cfg.Wait)
for range debouncedChan {
watchers = append(watchers, watcher)
debouncedChan := newDebounceChannel(watcher, config.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)
changed := GenerateFile(config, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", cfg.Dest, cfg.NotifyCmd)
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
continue
}
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
}
}(cfg)
}(config, make(chan *docker.APIEvents, 100))
}
// 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()
sigChan := newSignalChannel()
for {
watching := false
if client == nil {
var err error
endpoint, err := dockerclient.GetEndpoint(g.Endpoint)
endpoint, err := 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)
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)
@ -299,7 +291,7 @@ func (g *generator) generateFromEvents() {
case sig := <-sigChan:
log.Printf("Received signal: %s\n", sig)
switch sig {
case syscall.SIGTERM, syscall.SIGINT:
case syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT:
// close all watchers and exit
for _, watcher := range watchers {
close(watcher)
@ -312,7 +304,7 @@ func (g *generator) generateFromEvents() {
}()
}
func (g *generator) runNotifyCmd(config config.Config) {
func (g *generator) runNotifyCmd(config Config) {
if config.NotifyCmd == "" {
return
}
@ -332,24 +324,16 @@ func (g *generator) runNotifyCmd(config config.Config) {
}
}
func (g *generator) sendSignalToContainer(config config.Config) {
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)
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),
Signal: signal,
}
if err := g.Client.KillContainer(killOpts); err != nil {
log.Printf("Error sending signal to container: %s", err)
@ -357,12 +341,12 @@ func (g *generator) sendSignalToContainer(config config.Config) {
}
}
func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
func (g *generator) getContainers() ([]*RuntimeContainer, error) {
apiInfo, err := g.Client.Info()
if err != nil {
log.Printf("Error retrieving docker server info: %s\n", err)
} else {
context.SetServerInfo(apiInfo)
SetServerInfo(apiInfo)
}
apiContainers, err := g.Client.ListContainers(docker.ListContainersOptions{
@ -373,41 +357,40 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
return nil, err
}
containers := []*context.RuntimeContainer{}
containers := []*RuntimeContainer{}
for _, apiContainer := range apiContainers {
opts := docker.InspectContainerOptions{ID: apiContainer.ID}
container, err := g.Client.InspectContainerWithOptions(opts)
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 := dockerclient.SplitDockerImage(container.Config.Image)
runtimeContainer := &context.RuntimeContainer{
registry, repository, tag := splitDockerImage(container.Config.Image)
runtimeContainer := &RuntimeContainer{
ID: container.ID,
Image: context.DockerImage{
Image: DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
},
State: context.State{
State: State{
Running: container.State.Running,
},
Name: strings.TrimLeft(container.Name, "/"),
Hostname: container.Config.Hostname,
Gateway: container.NetworkSettings.Gateway,
Addresses: []context.Address{},
Networks: []context.Network{},
Addresses: []Address{},
Networks: []Network{},
Env: make(map[string]string),
Volumes: make(map[string]context.Volume),
Node: context.SwarmNode{},
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 := context.Address{
address := Address{
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
@ -423,7 +406,7 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
}
for k, v := range container.NetworkSettings.Networks {
network := context.Network{
network := Network{
IP: v.IPAddress,
Name: k,
Gateway: v.Gateway,
@ -439,7 +422,7 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
network)
}
for k, v := range container.Volumes {
runtimeContainer.Volumes[k] = context.Volume{
runtimeContainer.Volumes[k] = Volume{
Path: k,
HostPath: v,
ReadWrite: container.VolumesRW[k],
@ -448,13 +431,13 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
if container.Node != nil {
runtimeContainer.Node.ID = container.Node.ID
runtimeContainer.Node.Name = container.Node.Name
runtimeContainer.Node.Address = context.Address{
runtimeContainer.Node.Address = Address{
IP: container.Node.IP,
}
}
for _, v := range container.Mounts {
runtimeContainer.Mounts = append(runtimeContainer.Mounts, context.Mount{
runtimeContainer.Mounts = append(runtimeContainer.Mounts, Mount{
Name: v.Name,
Source: v.Source,
Destination: v.Destination,
@ -464,7 +447,7 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
})
}
runtimeContainer.Env = utils.SplitKeyValueSlice(container.Config.Env)
runtimeContainer.Env = splitKeyValueSlice(container.Config.Env)
runtimeContainer.Labels = container.Config.Labels
containers = append(containers, runtimeContainer)
}
@ -472,13 +455,14 @@ func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
}
func newSignalChannel() (<-chan os.Signal, func()) {
func newSignalChannel() <-chan os.Signal {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
return sig, func() { signal.Stop(sig) }
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL)
return sig
}
func newDebounceChannel(input chan *docker.APIEvents, wait *config.Wait) chan *docker.APIEvents {
func newDebounceChannel(input chan *docker.APIEvents, wait *Wait) chan *docker.APIEvents {
if wait == nil {
return input
}

View File

@ -1,29 +1,25 @@
package generator
package dockergen
import (
"bufio"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"sync/atomic"
"testing"
"time"
docker "github.com/fsouza/go-dockerclient"
"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)
log.SetOutput(ioutil.Discard)
containerID := "8dfafdbc3a40"
var counter atomic.Int32
counter := 0
eventsResponse := `
{"status":"start","id":"8dfafdbc3a40","from":"base:latest","time":1374067924}
@ -39,7 +35,7 @@ func TestGenerateFromEvents(t *testing.T) {
for rsc.Scan() {
w.Write([]byte(rsc.Text()))
w.(http.Flusher).Flush()
time.Sleep(150 * time.Millisecond)
time.Sleep(15 * time.Millisecond)
}
time.Sleep(500 * time.Millisecond)
}))
@ -53,7 +49,7 @@ func TestGenerateFromEvents(t *testing.T) {
}))
server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := []docker.APIContainers{
{
docker.APIContainers{
ID: containerID,
Image: "base:latest",
Command: "/bin/sh",
@ -68,7 +64,7 @@ func TestGenerateFromEvents(t *testing.T) {
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)
counter++
container := docker.Container{
Name: "docker-gen-test",
ID: containerID,
@ -91,7 +87,7 @@ func TestGenerateFromEvents(t *testing.T) {
},
Image: "0ff407d5a7d9ed36acdf3e75de8cc127afecc9af234d05486be2981cdc01a38d",
NetworkSettings: &docker.NetworkSettings{
IPAddress: "10.0.0.10",
IPAddress: fmt.Sprintf("10.0.0.10"),
IPPrefixLen: 24,
Gateway: "10.0.0.1",
Bridge: "docker0",
@ -106,13 +102,13 @@ func TestGenerateFromEvents(t *testing.T) {
}))
serverURL := fmt.Sprintf("tcp://%s", strings.TrimRight(strings.TrimPrefix(server.URL(), "http://"), "/"))
client, err := dockerclient.NewDockerClient(serverURL, false, "", "", "")
client, err := 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")
tmplFile, err := ioutil.TempFile(os.TempDir(), "docker-gen-tmpl")
if err != nil {
t.Errorf("Failed to create temp file: %v\n", err)
}
@ -120,14 +116,14 @@ func TestGenerateFromEvents(t *testing.T) {
tmplFile.Close()
os.Remove(tmplFile.Name())
}()
err = os.WriteFile(tmplFile.Name(), []byte("{{range $key, $value := .}}{{$value.ID}}.{{$value.Env.COUNTER}}{{end}}"), 0644)
err = ioutil.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")
destFile, err := ioutil.TempFile(os.TempDir(), "docker-gen-out")
if err != nil {
t.Errorf("Failed to create temp file: %v\n", err)
}
@ -144,35 +140,35 @@ func TestGenerateFromEvents(t *testing.T) {
if err != nil {
t.Errorf("Failed to retrieve docker server version info: %v\n", err)
}
context.SetDockerEnv(apiVersion) // prevents a panic
SetDockerEnv(apiVersion) // prevents a panic
generator := &generator{
Client: client,
Endpoint: serverURL,
Configs: config.ConfigFile{
Config: []config.Config{
{
Configs: ConfigFile{
[]Config{
Config{
Template: tmplFile.Name(),
Dest: destFiles[0].Name(),
Watch: false,
},
{
Config{
Template: tmplFile.Name(),
Dest: destFiles[1].Name(),
Watch: true,
Wait: &config.Wait{Min: 0, Max: 0},
Wait: &Wait{0, 0},
},
{
Config{
Template: tmplFile.Name(),
Dest: destFiles[2].Name(),
Watch: true,
Wait: &config.Wait{Min: 200 * time.Millisecond, Max: 250 * time.Millisecond},
Wait: &Wait{20 * time.Millisecond, 25 * time.Millisecond},
},
{
Config{
Template: tmplFile.Name(),
Dest: destFiles[3].Name(),
Watch: true,
Wait: &config.Wait{Min: 250 * time.Millisecond, Max: 1 * time.Second},
Wait: &Wait{25 * time.Millisecond, 100 * time.Millisecond},
},
},
},
@ -189,12 +185,12 @@ func TestGenerateFromEvents(t *testing.T) {
// 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
// init 0ms 5ms 10ms 15ms 20ms 25ms 30ms 35ms 40ms 45ms 50ms 55ms
// ├──────╫──────┼──────┼──────╫──────┼──────┼──────╫──────┼──────┼──────┼──────┼──────┤
// File0 ├─ 1 ║ ║ ║
// File1 ├─ 1 ╟─ 2 ╟─ 3 ╟─ 5
// File2 ├─ 1 ╟───── max (250ms) ──║───────────> 4 ╟─────── min (200ms) ─────> 6
// File3 └─ 1 ╟──────────────────> ╟──────────────────> ╟─────────── min (250ms) ────────> 7
// File2 ├─ 1 ╟───── max (25ms) ──║───────────> 4 ╟─────── min (20ms) ─────> 6
// File3 └─ 1 ╟──────────────────> ╟──────────────────> ╟─────────── min (25ms) ────────> 7
// ┌───╨───┐ ┌───╨──┐ ┌───╨───┐
// │ start │ │ stop │ │ start │
// └───────┘ └──────┘ └───────┘
@ -202,7 +198,7 @@ func TestGenerateFromEvents(t *testing.T) {
expectedCounters := []int{1, 5, 6, 7}
for i, counter := range expectedCounters {
value, _ = os.ReadFile(destFiles[i].Name())
value, _ = ioutil.ReadFile(destFiles[i].Name())
expected = fmt.Sprintf("%s.%d", containerID, counter)
if string(value) != expected {
t.Errorf("expected: %s. got: %s", expected, value)

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,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,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,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)
}
}

576
template.go Normal file
View File

@ -0,0 +1,576 @@
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 read(path string) (string, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
b, err := ioutil.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 = 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{}, 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 := reflect.Indirect(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 {
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.(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)
})
}
// 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)
})
}
// 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, 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 {
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
}
// 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,
"read": read,
"first": arrayFirst,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"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,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
func filterRunning(config Config, containers Context) Context {
if config.IncludeStopped {
return containers
} else {
filteredContainers := Context{}
for _, container := range containers {
if container.State.Running {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers
}
}
func GenerateFile(config Config, containers Context) bool {
filteredRunningContainers := filterRunning(config, containers)
filteredContainers := 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 := 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()
}

865
template_test.go Normal file
View File

@ -0,0 +1,865 @@
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 %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) {
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 TestGroupByLabel(t *testing.T) {
containers := []*RuntimeContainer{
&RuntimeContainer{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
&RuntimeContainer{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
&RuntimeContainer{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
&RuntimeContainer{
ID: "4",
},
&RuntimeContainer{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}
groups, err := groupByLabel(containers, "com.docker.compose.project")
if err != nil {
t.FailNow()
}
if len(groups) != 3 {
t.Fail()
}
if len(groups["one"]) != 2 {
t.Fail()
}
if len(groups[""]) != 1 {
t.Fail()
}
if len(groups["two"]) != 1 {
t.FailNow()
}
if groups["two"][0].(RuntimeContainer).ID != "2" {
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 %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].(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 TestWhereNot(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{
{`{{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, "whereNot")
}
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.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, "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.
@ -18,7 +13,7 @@ upstream {{ $host }} {
{{ $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 }}
@ -67,4 +62,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.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 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)
}
}
}