From e54ee016b67afeeccdff758c3f63d261124944a7 Mon Sep 17 00:00:00 2001 From: Matthew McPherrin Date: Fri, 17 Nov 2023 13:12:02 -0500 Subject: [PATCH] Add Dockerfile, compose.yaml, and an unbound example config (#66) This adds a Dockerfile for unbound exporter, which we can publish in the future. An example docker compose.yml is included to demonstrate and test using it with unbound, along with a sample configuration file for unbound showing how to set up the remote-control. The unbound example config file is based on the one inside the mvance/docker image that's used here. A small integration test runs against the docker-compose setup to smoke-test unbound_exporter. --- .github/workflows/integration.yaml | 27 ++++++++++ Dockerfile | 20 +++++++ docker-compose.yml | 21 ++++++++ integration_test.go | 52 ++++++++++++++++++ unbound-example.conf | 84 ++++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 .github/workflows/integration.yaml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 integration_test.go create mode 100644 unbound-example.conf diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 0000000..912f078 --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,27 @@ +--- +name: integration + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + integration: + runs-on: [ubuntu-latest] + steps: + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: "1.21.x" + - name: checkout + uses: actions/checkout@v4 + - name: Start containers + run: docker compose up --build --detach + - name: run integration test + run: go test -v --tags=integration + - name: Stop containers + if: always() + run: docker compose down diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3977962 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21.4-bookworm AS build + +WORKDIR /go/src/app + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY *.go . + +ENV CGO_ENABLED=0 + +RUN GOOS=$TARGETOS GOARCH=$TARGETPLATFORM go build -v -o /go/bin/unbound_exporter ./... + +FROM gcr.io/distroless/static-debian12 + +COPY --from=build /go/bin/unbound_exporter / + +ENTRYPOINT ["/unbound_exporter"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a22a2f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + unbound_exporter: + build: . + command: [ "-unbound.host=unix:///var/run/socket/unbound.ctl" ] + volumes: + - socket:/var/run/socket:ro + ports: + - "9167:9167" + depends_on: + unbound: + condition: service_started + unbound: + image: "mvance/unbound:1.18.0" + volumes: + - socket:/var/run/socket:rw + - ./unbound-example.conf:/opt/unbound/etc/unbound/unbound.conf + ports: + - "1053:1053/udp" + - "1053:1053/tcp" +volumes: + socket: diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..f0530ef --- /dev/null +++ b/integration_test.go @@ -0,0 +1,52 @@ +//go:build integration + +package main + +import ( + "net/http" + "testing" + + "github.com/prometheus/common/expfmt" +) + +// TestIntegration checks that unbound_exporter is running, successfully +// scraping and exporting metrics. +// +// It assumes unbound_exporter is available on localhost:9167, and Unbound on +// localhost:1053, as is set up in the docker-compose.yml file. +// +// A typical invocation of this test would look like +// +// docker compose up --build -d +// go test --tags=integration +// docker compose down +func TestIntegration(t *testing.T) { + resp, err := http.Get("http://localhost:9167/metrics") + if err != nil { + t.Fatalf("Failed to fetch metrics from unbound_exporter: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Expected a 200 OK from unbound_exporter, got: %v", resp.StatusCode) + } + + parser := expfmt.TextParser{} + metrics, err := parser.TextToMetricFamilies(resp.Body) + if err != nil { + t.Fatalf("Failed to parse metrics from unbound_exporter: %v", err) + } + + // unbound_up is 1 if we've successfully scraped metrics from it + unbound_up := metrics["unbound_up"].Metric[0].Gauge.GetValue() + if unbound_up != 1 { + t.Errorf("Expected unbound_up to be 1, not: %v", unbound_up) + } + + // Check some expected metrics are present + for _, metric := range []string{"go_info", "unbound_queries_total", "unbound_response_time_seconds", "unbound_cache_hits_total"} { + if _, ok := metrics[metric]; !ok { + t.Errorf("Expected metric is missing: %s", metric) + } + } +} diff --git a/unbound-example.conf b/unbound-example.conf new file mode 100644 index 0000000..991826e --- /dev/null +++ b/unbound-example.conf @@ -0,0 +1,84 @@ +## This is an example Unbound configuration file +## This is needed to use unbound_exporter +remote-control: + control-enable: yes + control-interface: /var/run/socket/unbound.ctl + +# The rest of this file is standard Unbound configuration +# There's nothing special here. +server: + cache-max-ttl: 86400 + cache-min-ttl: 300 + directory: "/opt/unbound/etc/unbound" + do-ip4: yes + do-ip6: no + do-tcp: yes + do-udp: yes + edns-buffer-size: 1232 + interface: 0.0.0.0 + port: 1053 + prefer-ip6: no + rrset-roundrobin: yes + username: "_unbound" + log-local-actions: no + log-queries: no + log-replies: no + log-servfail: yes + logfile: /opt/unbound/etc/unbound/unbound.log + verbosity: 2 + infra-cache-slabs: 4 + incoming-num-tcp: 10 + key-cache-slabs: 4 + msg-cache-size: 142768128 + msg-cache-slabs: 4 + num-queries-per-thread: 4096 + num-threads: 3 + outgoing-range: 8192 + rrset-cache-size: 285536256 + rrset-cache-slabs: 4 + minimal-responses: yes + prefetch: yes + prefetch-key: yes + serve-expired: yes + so-reuseport: yes + aggressive-nsec: yes + delay-close: 10000 + do-daemonize: no + do-not-query-localhost: no + neg-cache-size: 4M + qname-minimisation: yes + access-control: 127.0.0.1/32 allow + access-control: 192.168.0.0/16 allow + access-control: 172.16.0.0/12 allow + access-control: 10.0.0.0/8 allow + access-control: fc00::/7 allow + access-control: ::1/128 allow + auto-trust-anchor-file: "var/root.key" + chroot: "" + deny-any: yes + harden-algo-downgrade: yes + harden-below-nxdomain: yes + harden-dnssec-stripped: yes + harden-glue: yes + harden-large-queries: yes + harden-referral-path: no + harden-short-bufsize: yes + hide-http-user-agent: no + hide-identity: yes + hide-version: yes + http-user-agent: "DNS" + identity: "DNS" + private-address: 10.0.0.0/8 + private-address: 172.16.0.0/12 + private-address: 192.168.0.0/16 + private-address: 169.254.0.0/16 + private-address: fd00::/8 + private-address: fe80::/10 + private-address: ::ffff:0:0/96 + ratelimit: 1000 + tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt + unwanted-reply-threshold: 10000 + use-caps-for-id: yes + val-clean-additional: yes + include: /opt/unbound/etc/unbound/a-records.conf + include: /opt/unbound/etc/unbound/srv-records.conf