mirror of
https://github.com/coredns/coredns.git
synced 2025-10-26 15:54:16 -04:00
plugin/nomad: Add a Nomad plugin (#7467)
Signed-off-by: Olli Janatuinen <olli.janatuinen@gmail.com>
This commit is contained in:
@@ -64,4 +64,5 @@ var Directives = []string{
|
||||
"on",
|
||||
"sign",
|
||||
"view",
|
||||
"nomad",
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
_ "github.com/coredns/coredns/plugin/metrics"
|
||||
_ "github.com/coredns/coredns/plugin/minimal"
|
||||
_ "github.com/coredns/coredns/plugin/multisocket"
|
||||
_ "github.com/coredns/coredns/plugin/nomad"
|
||||
_ "github.com/coredns/coredns/plugin/nsid"
|
||||
_ "github.com/coredns/coredns/plugin/pprof"
|
||||
_ "github.com/coredns/coredns/plugin/quic"
|
||||
|
||||
8
go.mod
8
go.mod
@@ -22,6 +22,7 @@ require (
|
||||
github.com/farsightsec/golang-framestream v0.3.0
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645
|
||||
github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38 // v1.10.5
|
||||
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4
|
||||
github.com/miekg/dns v1.1.68
|
||||
@@ -115,7 +116,13 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/hashicorp/cronexpr v1.1.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -123,6 +130,7 @@ require (
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
|
||||
21
go.sum
21
go.sum
@@ -127,6 +127,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dnstap/golang-dnstap v0.4.0 h1:KRHBoURygdGtBjDI2w4HifJfMAhhOqDuktAokaSa234=
|
||||
github.com/dnstap/golang-dnstap v0.4.0/go.mod h1:FqsSdH58NAmkAvKcpyxht7i4FoBjKu8E4JUPt8ipSUs=
|
||||
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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -207,12 +209,27 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4=
|
||||
github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38 h1:1LTbcTpGdSdbj0ee7YZHNe4R2XqxfyWwIkSGWRhgkfM=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38/go.mod h1:0Tdp+9HbvwrxprXv/LfYZ8P21bOl4oA8Afyet1kUvhI=
|
||||
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 h1:w66aaP3c6SIQ0pi3QH1Tb4AMO3aWoEPxd1CNvLphbkA=
|
||||
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -243,6 +260,8 @@ github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -306,6 +325,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/shoenig/test v1.12.1 h1:mLHfnMv7gmhhP44WrvT+nKSxKkPDiNkIuHGdIGI9RLU=
|
||||
github.com/shoenig/test v1.12.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
|
||||
292
man/coredns-nomad.7
Normal file
292
man/coredns-nomad.7
Normal file
@@ -0,0 +1,292 @@
|
||||
.\" Generated by Mmark Markdown Processer - mmark.miek.nl
|
||||
.TH "COREDNS-NOMAD" 7 "September 2025" "CoreDNS" "CoreDNS Plugins"
|
||||
|
||||
.SH "NAME"
|
||||
.PP
|
||||
\fInomad\fP - enables reading zone data from a Nomad cluster.
|
||||
|
||||
.SH "DESCRIPTION"
|
||||
.PP
|
||||
This plugin serves DNS records for services registered with Nomad. Nomad 1.3+ comes with support for discovering services
|
||||
\[la]https://www.hashicorp.com/en/blog/nomad-service-discovery\[ra] with an in-built service catalogue that is available via the HTTP API. This plugin extends the HTTP API and provides a DNS interface for querying the service catalogue.
|
||||
|
||||
.PP
|
||||
The query can be looked up with the format \fB\fC[service].[namespace].service.nomad\fR. The plugin currently handles A, AAAA and SRV records. Refer to #Usage Example
|
||||
\[la]#usage-example\[ra] for more details.
|
||||
|
||||
.SH "EXAMPLE JOB TEMPLATE"
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
job "dns" {
|
||||
type = "service"
|
||||
|
||||
group "dns" {
|
||||
network {
|
||||
port "dns" {
|
||||
static = 1053
|
||||
}
|
||||
}
|
||||
task "dns" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "coredns/coredns:latest"
|
||||
ports = ["dns"]
|
||||
args = ["\-conf", "/secrets/coredns/Corefile", "\-dns.port", "1053"]
|
||||
}
|
||||
|
||||
service {
|
||||
name = "hostmaster"
|
||||
provider = "nomad"
|
||||
port = "dns"
|
||||
address\_mode = "driver"
|
||||
}
|
||||
|
||||
identity {
|
||||
env = true
|
||||
}
|
||||
|
||||
template {
|
||||
data = <<EOF
|
||||
\&. {
|
||||
forward . 1.1.1.1
|
||||
}
|
||||
|
||||
service.nomad. {
|
||||
errors
|
||||
debug
|
||||
health
|
||||
log
|
||||
nomad service.nomad {
|
||||
address unix:///secrets/api.sock
|
||||
ttl 10
|
||||
}
|
||||
cache 30
|
||||
}
|
||||
EOF
|
||||
destination = "secrets/coredns/Corefile"
|
||||
change\_mode = "signal"
|
||||
change\_signal = "SIGHUP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.SH "SYNTAX"
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
nomad [ZONE] {
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.PP
|
||||
With only the plugin specified, the \fInomad\fP plugin will default to \fB\fCservice.nomad\fR zone.
|
||||
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
nomad [ZONE] {
|
||||
address URL
|
||||
token TOKEN
|
||||
ttl DURATION
|
||||
}
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.IP \(bu 4
|
||||
\fB\fCaddress\fR The address where a Nomad agent (server) is available. \fBURL\fP defaults to \fB\fChttp://127.0.0.1:4646\fR.
|
||||
.IP \(bu 4
|
||||
\fB\fCtoken\fR The SecretID of an ACL token to use to authenticate API requests with if the Nomad cluster has ACL enabled. \fBTOKEN\fP defaults to \fB\fC""\fR.
|
||||
.IP \(bu 4
|
||||
\fB\fCttl\fR allows you to set a custom TTL for responses. \fBDURATION\fP defaults to \fB\fC30 seconds\fR. The minimum TTL allowed is \fB\fC0\fR seconds, and the maximum is capped at \fB\fC3600\fR seconds. Setting TTL to 0 will prevent records from being cached. The unit for the value is seconds.
|
||||
|
||||
|
||||
.SH "METRICS"
|
||||
.PP
|
||||
If monitoring is enabled (via the \fIprometheus\fP directive) the following metric is exported:
|
||||
|
||||
.IP \(bu 4
|
||||
\fB\fCcoredns_nomad_success_requests_total{namespace,server}\fR - Counter of DNS requests handled successfully.
|
||||
.IP \(bu 4
|
||||
\fB\fCcoredns_nomad_failed_requests_total{namespace,server}\fR - Counter of DNS requests failed.
|
||||
|
||||
|
||||
.PP
|
||||
The \fB\fCserver\fR label indicated which server handled the request. \fB\fCnamespace\fR indicates the namespace of the service in the query.
|
||||
|
||||
.SH "READY"
|
||||
.PP
|
||||
This plugin reports readiness to the ready plugin. It will be ready only when it has successfully connected to the Nomad server. It queries the \fB\fC/v1/agent/self\fR
|
||||
\[la]https://developer.hashicorp.com/nomad/api-docs/agent#query-self\[ra] endpoint to check if it is ready.
|
||||
|
||||
.SH "EXAMPLES"
|
||||
.PP
|
||||
Enable nomad with and resolve all services with \fB\fC.nomad\fR as the suffix. \fB\fCcache\fR plugin is used to cache the responses for 30 seconds. This avoids a lookup to the Nomad server for every request.
|
||||
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
service.nomad.:1053 {
|
||||
log
|
||||
cache
|
||||
errors
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646 http://127.0.0.2:4646 http://127.0.0.3:4646
|
||||
ttl 10
|
||||
}
|
||||
cache 30
|
||||
}
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.SH "AUTHENTICATION"
|
||||
.PP
|
||||
\fB\fCnomad\fR plugin uses a default Nomad configuration to create an API client. Options like the HTTP address and the token can be specified in Corefile. However, Nomad Go SDK can also additionally read these environment variables.
|
||||
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_TOKEN\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_ADDR\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_REGION\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_NAMESPACE\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_HTTP_AUTH\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_CACERT\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_CAPATH\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_CLIENT_CERT\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_CLIENT_KEY\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_TLS_SERVER_NAME\fR
|
||||
.IP \(bu 4
|
||||
\fB\fCNOMAD_SKIP_VERIFY\fR
|
||||
|
||||
|
||||
.PP
|
||||
You can read about them in detail here
|
||||
\[la]https://developer.hashicorp.com/nomad/docs/reference/runtime-environment-settings\[ra].
|
||||
|
||||
.SH "USAGE EXAMPLE"
|
||||
.SS "A RECORD"
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
dig redis.default.service.nomad @127.0.0.1 \-p 1053
|
||||
|
||||
; <<>> DiG 9.18.1\-1ubuntu1.2\-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 \-p 1053
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; \->>HEADER<<\- opcode: QUERY, status: NOERROR, id: 54986
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: bdc9237f49a1f744 (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;redis.default.service.nomad. IN A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
|
||||
;; Query time: 4 msec
|
||||
;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP)
|
||||
;; WHEN: Thu Jan 05 12:12:25 IST 2023
|
||||
;; MSG SIZE rcvd: 165
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.SS "SRV RECORD"
|
||||
.PP
|
||||
Since an A record doesn't contain the port number, SRV record can be used to query the port number of a service.
|
||||
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
dig redis.default.service.nomad @127.0.0.1 \-p 1053 SRV
|
||||
|
||||
; <<>> DiG 9.18.1\-1ubuntu1.2\-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 \-p 1053 SRV
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; \->>HEADER<<\- opcode: QUERY, status: NOERROR, id: 49945
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 4
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: 14572535f3ba6648 (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;redis.default.service.nomad. IN SRV
|
||||
|
||||
;; ANSWER SECTION:
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 25395 redis.default.service.nomad.
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 20888 redis.default.service.nomad.
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 26292 redis.default.service.nomad.
|
||||
|
||||
;; ADDITIONAL SECTION:
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
|
||||
;; Query time: 0 msec
|
||||
;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP)
|
||||
;; WHEN: Thu Jan 05 12:12:20 IST 2023
|
||||
;; MSG SIZE rcvd: 339
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.SS "SOA RECORD"
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
$ dig @localhost \-p 1053 1dns.default.service.nomad.
|
||||
|
||||
; <<>> DiG 9.18.12\-0ubuntu0.22.04.2\-Ubuntu <<>> @localhost \-p 1053 1dns.default.service.nomad.
|
||||
; (1 server found)
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; \->>HEADER<<\- opcode: QUERY, status: NXDOMAIN, id: 21012
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: 6d146bb140b4d8ca (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;1dns.default.service.nomad. IN A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
1dns.default.service.nomad. 5 IN SOA ns1.1dns.default.service.nomad. ns1.1dns.default.service.nomad. 1 3600 600 604800 3600
|
||||
|
||||
;; Query time: 0 msec
|
||||
;; SERVER: 127.0.0.1#1053(localhost) (UDP)
|
||||
;; WHEN: Wed Aug 23 21:14:41 EEST 2023
|
||||
;; MSG SIZE rcvd: 189
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
@@ -73,3 +73,4 @@ whoami:whoami
|
||||
on:github.com/coredns/caddy/onevent
|
||||
sign:sign
|
||||
view:view
|
||||
nomad:nomad
|
||||
|
||||
235
plugin/nomad/README.md
Normal file
235
plugin/nomad/README.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# nomad
|
||||
|
||||
## Name
|
||||
|
||||
*nomad* - enables reading zone data from a Nomad cluster.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin serves DNS records for services registered with Nomad. Nomad 1.3+ comes with [support for discovering services](https://www.hashicorp.com/en/blog/nomad-service-discovery) with an in-built service catalogue that is available via the HTTP API. This plugin extends the HTTP API and provides a DNS interface for querying the service catalogue.
|
||||
|
||||
The query can be looked up with the format `[service].[namespace].service.nomad`. The plugin currently handles A, AAAA and SRV records. Refer to [#Usage Example](#usage-example) for more details.
|
||||
|
||||
## Example job template
|
||||
|
||||
```
|
||||
job "dns" {
|
||||
type = "service"
|
||||
|
||||
group "dns" {
|
||||
network {
|
||||
port "dns" {
|
||||
static = 1053
|
||||
}
|
||||
}
|
||||
task "dns" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "coredns/coredns:latest"
|
||||
ports = ["dns"]
|
||||
args = ["-conf", "/secrets/coredns/Corefile", "-dns.port", "1053"]
|
||||
}
|
||||
|
||||
service {
|
||||
name = "hostmaster"
|
||||
provider = "nomad"
|
||||
port = "dns"
|
||||
address_mode = "driver"
|
||||
}
|
||||
|
||||
identity {
|
||||
env = true
|
||||
}
|
||||
|
||||
template {
|
||||
data = <<EOF
|
||||
. {
|
||||
forward . 1.1.1.1
|
||||
}
|
||||
|
||||
service.nomad. {
|
||||
errors
|
||||
debug
|
||||
health
|
||||
log
|
||||
nomad service.nomad {
|
||||
address unix:///secrets/api.sock
|
||||
ttl 10
|
||||
}
|
||||
cache 30
|
||||
}
|
||||
EOF
|
||||
destination = "secrets/coredns/Corefile"
|
||||
change_mode = "signal"
|
||||
change_signal = "SIGHUP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
nomad [ZONE] {
|
||||
~~~
|
||||
|
||||
With only the plugin specified, the *nomad* plugin will default to `service.nomad` zone.
|
||||
|
||||
~~~ txt
|
||||
nomad [ZONE] {
|
||||
address URL
|
||||
token TOKEN
|
||||
ttl DURATION
|
||||
}
|
||||
~~~
|
||||
|
||||
* `address` The address where a Nomad agent (server) is available. **URL** defaults to `http://127.0.0.1:4646`.
|
||||
|
||||
* `token` The SecretID of an ACL token to use to authenticate API requests with if the Nomad cluster has ACL enabled. **TOKEN** defaults to `""`.
|
||||
|
||||
* `ttl` allows you to set a custom TTL for responses. **DURATION** defaults to `30 seconds`. The minimum TTL allowed is `0` seconds, and the maximum is capped at `3600` seconds. Setting TTL to 0 will prevent records from being cached. The unit for the value is seconds.
|
||||
|
||||
## Metrics
|
||||
|
||||
If monitoring is enabled (via the *prometheus* directive) the following metric is exported:
|
||||
|
||||
* `coredns_nomad_success_requests_total{namespace,server}` - Counter of DNS requests handled successfully.
|
||||
* `coredns_nomad_failed_requests_total{namespace,server}` - Counter of DNS requests failed.
|
||||
|
||||
The `server` label indicated which server handled the request. `namespace` indicates the namespace of the service in the query.
|
||||
|
||||
## Ready
|
||||
|
||||
This plugin reports readiness to the ready plugin. It will be ready only when it has successfully connected to the Nomad server. It queries the [`/v1/agent/self`](https://developer.hashicorp.com/nomad/api-docs/agent#query-self) endpoint to check if it is ready.
|
||||
|
||||
## Examples
|
||||
|
||||
Enable nomad with and resolve all services with `.nomad` as the suffix. `cache` plugin is used to cache the responses for 30 seconds. This avoids a lookup to the Nomad server for every request.
|
||||
|
||||
```
|
||||
service.nomad.:1053 {
|
||||
log
|
||||
cache
|
||||
errors
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646 http://127.0.0.2:4646 http://127.0.0.3:4646
|
||||
ttl 10
|
||||
}
|
||||
cache 30
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
`nomad` plugin uses a default Nomad configuration to create an API client. Options like the HTTP address and the token can be specified in Corefile. However, Nomad Go SDK can also additionally read these environment variables.
|
||||
|
||||
- `NOMAD_TOKEN`
|
||||
- `NOMAD_ADDR`
|
||||
- `NOMAD_REGION`
|
||||
- `NOMAD_NAMESPACE`
|
||||
- `NOMAD_HTTP_AUTH`
|
||||
- `NOMAD_CACERT`
|
||||
- `NOMAD_CAPATH`
|
||||
- `NOMAD_CLIENT_CERT`
|
||||
- `NOMAD_CLIENT_KEY`
|
||||
- `NOMAD_TLS_SERVER_NAME`
|
||||
- `NOMAD_SKIP_VERIFY`
|
||||
|
||||
You can read about them in detail [here](https://developer.hashicorp.com/nomad/docs/reference/runtime-environment-settings).
|
||||
|
||||
## Usage Example
|
||||
|
||||
### A record
|
||||
|
||||
```
|
||||
dig redis.default.service.nomad @127.0.0.1 -p 1053
|
||||
|
||||
; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 -p 1053
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54986
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: bdc9237f49a1f744 (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;redis.default.service.nomad. IN A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 10 IN A 192.168.29.76
|
||||
|
||||
;; Query time: 4 msec
|
||||
;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP)
|
||||
;; WHEN: Thu Jan 05 12:12:25 IST 2023
|
||||
;; MSG SIZE rcvd: 165
|
||||
```
|
||||
|
||||
### SRV Record
|
||||
|
||||
Since an A record doesn't contain the port number, SRV record can be used to query the port number of a service.
|
||||
|
||||
```
|
||||
dig redis.default.service.nomad @127.0.0.1 -p 1053 SRV
|
||||
|
||||
; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 -p 1053 SRV
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49945
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 4
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: 14572535f3ba6648 (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;redis.default.service.nomad. IN SRV
|
||||
|
||||
;; ANSWER SECTION:
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 25395 redis.default.service.nomad.
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 20888 redis.default.service.nomad.
|
||||
redis.default.service.nomad. 8 IN SRV 10 10 26292 redis.default.service.nomad.
|
||||
|
||||
;; ADDITIONAL SECTION:
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
redis.default.service.nomad. 8 IN A 192.168.29.76
|
||||
|
||||
;; Query time: 0 msec
|
||||
;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP)
|
||||
;; WHEN: Thu Jan 05 12:12:20 IST 2023
|
||||
;; MSG SIZE rcvd: 339
|
||||
```
|
||||
|
||||
### SOA Record
|
||||
|
||||
```
|
||||
$ dig @localhost -p 1053 1dns.default.service.nomad.
|
||||
|
||||
; <<>> DiG 9.18.12-0ubuntu0.22.04.2-Ubuntu <<>> @localhost -p 1053 1dns.default.service.nomad.
|
||||
; (1 server found)
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 21012
|
||||
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
||||
;; WARNING: recursion requested but not available
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; COOKIE: 6d146bb140b4d8ca (echoed)
|
||||
;; QUESTION SECTION:
|
||||
;1dns.default.service.nomad. IN A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
1dns.default.service.nomad. 5 IN SOA ns1.1dns.default.service.nomad. ns1.1dns.default.service.nomad. 1 3600 600 604800 3600
|
||||
|
||||
;; Query time: 0 msec
|
||||
;; SERVER: 127.0.0.1#1053(localhost) (UDP)
|
||||
;; WHEN: Wed Aug 23 21:14:41 EEST 2023
|
||||
;; MSG SIZE rcvd: 189
|
||||
```
|
||||
68
plugin/nomad/helpers.go
Normal file
68
plugin/nomad/helpers.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func addSRVRecord(m *dns.Msg, s *api.ServiceRegistration, header dns.RR_Header, originalQName string, addr net.IP, ttl uint32) error {
|
||||
srvRecord := &dns.SRV{
|
||||
Hdr: header,
|
||||
Target: originalQName,
|
||||
Port: uint16(s.Port),
|
||||
Priority: 10,
|
||||
Weight: 10,
|
||||
}
|
||||
m.Answer = append(m.Answer, srvRecord)
|
||||
|
||||
if addr.To4() == nil {
|
||||
addExtrasToAAAARecord(m, originalQName, ttl, addr)
|
||||
} else {
|
||||
addExtrasToARecord(m, originalQName, ttl, addr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addExtrasToARecord(m *dns.Msg, originalQName string, ttl uint32, addr net.IP) {
|
||||
header := dns.RR_Header{
|
||||
Name: originalQName,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
}
|
||||
m.Extra = append(m.Extra, &dns.A{Hdr: header, A: addr})
|
||||
}
|
||||
|
||||
func addExtrasToAAAARecord(m *dns.Msg, originalQName string, ttl uint32, addr net.IP) {
|
||||
header := dns.RR_Header{
|
||||
Name: originalQName,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
}
|
||||
m.Extra = append(m.Extra, &dns.AAAA{Hdr: header, AAAA: addr})
|
||||
}
|
||||
|
||||
func addARecord(m *dns.Msg, header dns.RR_Header, addr net.IP) {
|
||||
m.Answer = append(m.Answer, &dns.A{Hdr: header, A: addr})
|
||||
}
|
||||
|
||||
func addAAAARecord(m *dns.Msg, header dns.RR_Header, addr net.IP) {
|
||||
m.Answer = append(m.Answer, &dns.AAAA{Hdr: header, AAAA: addr})
|
||||
}
|
||||
|
||||
func createSOARecord(originalQName string, ttl uint32, zone string) *dns.SOA {
|
||||
return &dns.SOA{
|
||||
Hdr: dns.RR_Header{Name: dns.Fqdn(originalQName), Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: ttl},
|
||||
Ns: dns.Fqdn("ns1." + originalQName),
|
||||
Mbox: dns.Fqdn("hostmaster." + zone),
|
||||
Serial: 0,
|
||||
Refresh: 3600,
|
||||
Retry: 600,
|
||||
Expire: 86400,
|
||||
Minttl: 30,
|
||||
}
|
||||
}
|
||||
25
plugin/nomad/metrics.go
Normal file
25
plugin/nomad/metrics.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
// requestSuccessCount is the number of DNS requests handled successfully.
|
||||
requestSuccessCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: pluginName,
|
||||
Name: "success_requests_total",
|
||||
Help: "Counter of DNS requests handled successfully.",
|
||||
}, []string{"server", "namespace"})
|
||||
// requestFailedCount is the number of DNS requests that failed.
|
||||
requestFailedCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: pluginName,
|
||||
Name: "failed_requests_total",
|
||||
Help: "Counter of DNS requests failed.",
|
||||
}, []string{"server", "namespace"})
|
||||
)
|
||||
158
plugin/nomad/nomad.go
Normal file
158
plugin/nomad/nomad.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const pluginName = "nomad"
|
||||
|
||||
var (
|
||||
log = clog.NewWithPlugin(pluginName)
|
||||
defaultTTL = 30
|
||||
)
|
||||
|
||||
type Nomad struct {
|
||||
Next plugin.Handler
|
||||
|
||||
ttl uint32
|
||||
Zone string
|
||||
clients []*api.Client
|
||||
current int
|
||||
}
|
||||
|
||||
func (n *Nomad) Name() string {
|
||||
return pluginName
|
||||
}
|
||||
|
||||
func (n Nomad) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
qname, originalQName, err := processQName(state.Name(), n.Zone)
|
||||
if err != nil {
|
||||
return plugin.NextOrFailure(n.Name(), n.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
namespace, serviceName, err := extractNamespaceAndService(qname)
|
||||
if err != nil {
|
||||
return plugin.NextOrFailure(n.Name(), n.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
m, header := initializeMessage(state, n.ttl)
|
||||
|
||||
svcRegistrations, _, err := fetchServiceRegistrations(n, serviceName, namespace)
|
||||
if err != nil {
|
||||
log.Warning(err)
|
||||
return handleServiceLookupError(w, m, ctx, namespace)
|
||||
}
|
||||
|
||||
if len(svcRegistrations) == 0 {
|
||||
return handleResponseError(n, w, m, originalQName, n.ttl, ctx, namespace, err)
|
||||
}
|
||||
|
||||
if err := addServiceResponses(m, svcRegistrations, header, state.QType(), originalQName, n.ttl); err != nil {
|
||||
return handleResponseError(n, w, m, originalQName, n.ttl, ctx, namespace, err)
|
||||
}
|
||||
|
||||
err = w.WriteMsg(m)
|
||||
requestSuccessCount.WithLabelValues(metrics.WithServer(ctx), namespace).Inc()
|
||||
return dns.RcodeSuccess, err
|
||||
}
|
||||
|
||||
func processQName(qname, zone string) (string, string, error) {
|
||||
original := dns.Fqdn(qname)
|
||||
base, err := dnsutil.TrimZone(original, dns.Fqdn(zone))
|
||||
return base, original, err
|
||||
}
|
||||
|
||||
func extractNamespaceAndService(qname string) (string, string, error) {
|
||||
qnameSplit := dns.SplitDomainName(qname)
|
||||
if len(qnameSplit) < 2 {
|
||||
return "", "", fmt.Errorf("invalid query name")
|
||||
}
|
||||
return qnameSplit[1], qnameSplit[0], nil
|
||||
}
|
||||
|
||||
func initializeMessage(state request.Request, ttl uint32) (*dns.Msg, dns.RR_Header) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(state.Req)
|
||||
m.Authoritative, m.Compress, m.Rcode = true, true, dns.RcodeSuccess
|
||||
|
||||
header := dns.RR_Header{
|
||||
Name: state.QName(),
|
||||
Rrtype: state.QType(),
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
}
|
||||
|
||||
return m, header
|
||||
}
|
||||
|
||||
func fetchServiceRegistrations(n Nomad, serviceName, namespace string) ([]*api.ServiceRegistration, *api.QueryMeta, error) {
|
||||
log.Debugf("Looking up record for svc: %s namespace: %s", serviceName, namespace)
|
||||
nc, err := n.getClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nc.Services().Get(serviceName, (&api.QueryOptions{Namespace: namespace}))
|
||||
}
|
||||
|
||||
func handleServiceLookupError(w dns.ResponseWriter, m *dns.Msg, ctx context.Context, namespace string) (int, error) {
|
||||
m.Rcode = dns.RcodeSuccess
|
||||
err := w.WriteMsg(m)
|
||||
requestFailedCount.WithLabelValues(metrics.WithServer(ctx), namespace).Inc()
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
|
||||
func addServiceResponses(m *dns.Msg, svcRegistrations []*api.ServiceRegistration, header dns.RR_Header, qtype uint16, originalQName string, ttl uint32) error {
|
||||
for _, s := range svcRegistrations {
|
||||
addr := net.ParseIP(s.Address)
|
||||
if addr == nil {
|
||||
return fmt.Errorf("error parsing IP address")
|
||||
}
|
||||
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
if addr.To4() == nil {
|
||||
continue
|
||||
}
|
||||
addARecord(m, header, addr)
|
||||
case dns.TypeAAAA:
|
||||
if addr.To4() != nil {
|
||||
continue
|
||||
}
|
||||
addAAAARecord(m, header, addr)
|
||||
case dns.TypeSRV:
|
||||
err := addSRVRecord(m, s, header, originalQName, addr, ttl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
m.Rcode = dns.RcodeNotImplemented
|
||||
return fmt.Errorf("query type not implemented")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleResponseError(n Nomad, w dns.ResponseWriter, m *dns.Msg, originalQName string, ttl uint32, ctx context.Context, namespace string, err error) (int, error) {
|
||||
m.Rcode = dns.RcodeNameError
|
||||
m.Answer = append(m.Answer, createSOARecord(originalQName, ttl, n.Zone))
|
||||
|
||||
if writeErr := w.WriteMsg(m); writeErr != nil {
|
||||
return dns.RcodeServerFailure, fmt.Errorf("write message error: %w", writeErr)
|
||||
}
|
||||
|
||||
requestFailedCount.WithLabelValues(metrics.WithServer(ctx), namespace).Inc()
|
||||
|
||||
return dns.RcodeSuccess, err
|
||||
}
|
||||
204
plugin/nomad/nomad_test.go
Normal file
204
plugin/nomad/nomad_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
nomad "github.com/hashicorp/nomad/api"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestNomad(t *testing.T) {
|
||||
var demoNomad = Nomad{
|
||||
Next: test.ErrorHandler(),
|
||||
ttl: uint32(defaultTTL),
|
||||
Zone: "service.nomad",
|
||||
clients: make([]*nomad.Client, 0),
|
||||
}
|
||||
|
||||
var cases = []test.Case{
|
||||
{
|
||||
Qname: "example.default.service.nomad.",
|
||||
Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("example.default.service.nomad. 30 IN A 1.2.3.4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "example.default.service.nomad.",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{},
|
||||
},
|
||||
{
|
||||
Qname: "fakeipv6.default.service.nomad.",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.AAAA("fakeipv6.default.service.nomad. 30 IN AAAA 1:2:3::4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "fakeipv6.default.service.nomad.",
|
||||
Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{},
|
||||
},
|
||||
{
|
||||
Qname: "multi.default.service.nomad.",
|
||||
Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("multi.default.service.nomad. 30 IN A 1.2.3.4"),
|
||||
test.A("multi.default.service.nomad. 30 IN A 1.2.3.5"),
|
||||
test.A("multi.default.service.nomad. 30 IN A 1.2.3.6"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "nonexistent.default.service.nomad.",
|
||||
Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Answer: []dns.RR{
|
||||
test.SOA("nonexistent.default.service.nomad. 30 IN SOA ns1.nonexistent.default.service.nomad. hostmaster.service.nomad. 0 3600 600 86400 30"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "example.default.service.nomad.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{test.SRV("example.default.service.nomad. 30 IN SRV 10 10 23202 example.default.service.nomad.")},
|
||||
Extra: []dns.RR{test.A("example.default.service.nomad. 30 IN A 1.2.3.4")},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup a fake Nomad servers.
|
||||
nomadServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/service/example":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":23202,"ServiceName":"example"}]`))
|
||||
case "/v1/service/fakeipv6":
|
||||
w.Write([]byte(`[{"Address":"1:2:3::4","Namespace":"default","Port":8000,"ServiceName":"fakeipv6"}]`))
|
||||
case "/v1/service/multi":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":25395,"ServiceName":"multi"},{"Address":"1.2.3.5","Namespace":"default","Port":20888,"ServiceName":"multi"},{"Address":"1.2.3.6","Namespace":"default","Port":26292,"ServiceName":"multi"}]`))
|
||||
case "/v1/service/nonexistent":
|
||||
w.Write([]byte(`[]`))
|
||||
case "/v1/agent/self":
|
||||
w.Write([]byte(`{"Member":{"Name":"foobar1"}}`))
|
||||
}
|
||||
}))
|
||||
defer nomadServer1.Close()
|
||||
nomadServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/service/example":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":23202,"ServiceName":"example"}]`))
|
||||
case "/v1/service/fakeipv6":
|
||||
w.Write([]byte(`[{"Address":"1:2:3::4","Namespace":"default","Port":8000,"ServiceName":"fakeipv6"}]`))
|
||||
case "/v1/service/multi":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":25395,"ServiceName":"multi"},{"Address":"1.2.3.5","Namespace":"default","Port":20888,"ServiceName":"multi"},{"Address":"1.2.3.6","Namespace":"default","Port":26292,"ServiceName":"multi"}]`))
|
||||
case "/v1/service/nonexistent":
|
||||
w.Write([]byte(`[]`))
|
||||
case "/v1/agent/self":
|
||||
w.Write([]byte(`{"Member":{"Name":"foobar2"}}`))
|
||||
}
|
||||
}))
|
||||
defer nomadServer2.Close()
|
||||
nomadServer3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/service/example":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":23202,"ServiceName":"example"}]`))
|
||||
case "/v1/service/fakeipv6":
|
||||
w.Write([]byte(`[{"Address":"1:2:3::4","Namespace":"default","Port":8000,"ServiceName":"fakeipv6"}]`))
|
||||
case "/v1/service/multi":
|
||||
w.Write([]byte(`[{"Address":"1.2.3.4","Namespace":"default","Port":25395,"ServiceName":"multi"},{"Address":"1.2.3.5","Namespace":"default","Port":20888,"ServiceName":"multi"},{"Address":"1.2.3.6","Namespace":"default","Port":26292,"ServiceName":"multi"}]`))
|
||||
case "/v1/service/nonexistent":
|
||||
w.Write([]byte(`[]`))
|
||||
case "/v1/agent/self":
|
||||
w.Write([]byte(`{"Member":{"Name":"foobar3"}}`))
|
||||
}
|
||||
}))
|
||||
defer nomadServer3.Close()
|
||||
|
||||
// Configure the plugin to use the fake Nomad server.
|
||||
cfg := nomad.DefaultConfig()
|
||||
cfg.Address = nomadServer1.URL
|
||||
client1, err := nomad.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create Nomad client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
demoNomad.clients = append(demoNomad.clients, client1)
|
||||
|
||||
cfg = nomad.DefaultConfig()
|
||||
cfg.Address = nomadServer2.URL
|
||||
client2, err := nomad.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create Nomad client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
demoNomad.clients = append(demoNomad.clients, client2)
|
||||
|
||||
cfg = nomad.DefaultConfig()
|
||||
cfg.Address = nomadServer3.URL
|
||||
client3, err := nomad.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create Nomad client: %v", err)
|
||||
return
|
||||
}
|
||||
demoNomad.clients = append(demoNomad.clients, client3)
|
||||
|
||||
client, _ := demoNomad.getClient()
|
||||
if client != client1 {
|
||||
t.Errorf("Expected client1")
|
||||
}
|
||||
|
||||
nomadServer1.Close()
|
||||
|
||||
client, _ = demoNomad.getClient()
|
||||
if client == client1 {
|
||||
t.Errorf("Expected client2")
|
||||
}
|
||||
|
||||
client, err = demoNomad.getClient()
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Errorf("Expected client2 but got nil")
|
||||
}
|
||||
if client != client2 {
|
||||
t.Errorf("Expected client2")
|
||||
}
|
||||
|
||||
runTests(ctx, t, &demoNomad, cases)
|
||||
}
|
||||
|
||||
func runTests(ctx context.Context, t *testing.T, n *Nomad, cases []test.Case) {
|
||||
t.Helper()
|
||||
for i, tc := range cases {
|
||||
r := tc.Msg()
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
|
||||
_, err := n.ServeDNS(ctx, w, r)
|
||||
if err != tc.Error {
|
||||
t.Errorf("Test %d: %v (%v) (%v)", i, err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if w.Msg == nil {
|
||||
t.Errorf("Test %d: nil message", i)
|
||||
}
|
||||
if err := test.SortAndCheck(w.Msg, tc); err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
plugin/nomad/ready.go
Normal file
9
plugin/nomad/ready.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package nomad
|
||||
|
||||
// Ready signals when the plugin is ready for use.
|
||||
// In case of Nomad, when the ping to the Nomad API is successful
|
||||
// the plugin is ready.
|
||||
func (n Nomad) Ready() bool {
|
||||
client, _ := n.getClient()
|
||||
return client != nil
|
||||
}
|
||||
141
plugin/nomad/setup.go
Normal file
141
plugin/nomad/setup.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
nomad "github.com/hashicorp/nomad/api"
|
||||
)
|
||||
|
||||
// init registers this plugin.
|
||||
func init() { plugin.Register(pluginName, setup) }
|
||||
|
||||
// setup is the function that gets called when the config parser sees the token "nomad". Setup is responsible
|
||||
// for parsing any extra options the nomad plugin may have. The first token this function sees is "nomad".
|
||||
func setup(c *caddy.Controller) error {
|
||||
n := &Nomad{
|
||||
ttl: uint32(defaultTTL),
|
||||
clients: make([]*nomad.Client, 0),
|
||||
current: -1,
|
||||
}
|
||||
|
||||
// Parse the configuration, including the zone argument
|
||||
if err := parse(c, n); err != nil {
|
||||
return plugin.Error("nomad", err)
|
||||
}
|
||||
|
||||
c.OnStartup(func() error {
|
||||
var err error
|
||||
for idx, client := range n.clients {
|
||||
_, err := client.Agent().Self()
|
||||
if err == nil {
|
||||
n.current = idx
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
n.Next = next
|
||||
return n
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller, n *Nomad) error {
|
||||
var token string
|
||||
addresses := []string{} // Multiple addresses are stored here
|
||||
|
||||
// Expect the first token to be "nomad"
|
||||
if !c.Next() {
|
||||
return c.Err("expected 'nomad' token")
|
||||
}
|
||||
|
||||
// Check for the zone argument
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
n.Zone = "service.nomad"
|
||||
} else {
|
||||
n.Zone = args[0]
|
||||
}
|
||||
|
||||
// Parse the configuration block
|
||||
for c.NextBlock() {
|
||||
selector := strings.ToLower(c.Val())
|
||||
|
||||
switch selector {
|
||||
case "address":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return c.Err("at least one address is required")
|
||||
}
|
||||
addresses = append(addresses, args...)
|
||||
case "token":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.Err("exactly one token is required")
|
||||
}
|
||||
token = args[0]
|
||||
case "ttl":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.Err("exactly one ttl value is required")
|
||||
}
|
||||
t, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return c.Err("error parsing ttl: " + err.Error())
|
||||
}
|
||||
if t < 0 || t > 3600 {
|
||||
return c.Errf("ttl must be in range [0, 3600]: %d", t)
|
||||
}
|
||||
n.ttl = uint32(t)
|
||||
default:
|
||||
return c.Errf("unknown property '%s'", selector)
|
||||
}
|
||||
}
|
||||
|
||||
// Push an empty address to create a client solely based on the defaults.
|
||||
if len(addresses) == 0 {
|
||||
addresses = append(addresses, "")
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
cfg := nomad.DefaultConfig()
|
||||
if len(addr) > 0 {
|
||||
cfg.Address = addr
|
||||
}
|
||||
if len(token) > 0 {
|
||||
cfg.SecretID = token
|
||||
}
|
||||
client, err := nomad.NewClient(cfg)
|
||||
if err != nil {
|
||||
return plugin.Error("nomad", err)
|
||||
}
|
||||
n.clients = append(n.clients, client) // Store all clients
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nomad) getClient() (*nomad.Client, error) {
|
||||
// Don't bother querying Agent().Self() if there is only one client.
|
||||
if len(n.clients) == 1 {
|
||||
return n.clients[0], nil
|
||||
}
|
||||
for i := range len(n.clients) {
|
||||
idx := (n.current + i) % len(n.clients)
|
||||
_, err := n.clients[idx].Agent().Self()
|
||||
if err == nil {
|
||||
n.current = idx
|
||||
return n.clients[idx], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no Nomad client available")
|
||||
}
|
||||
110
plugin/nomad/setup_test.go
Normal file
110
plugin/nomad/setup_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
|
||||
nomad "github.com/hashicorp/nomad/api"
|
||||
)
|
||||
|
||||
func TestSetupNomad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
shouldErr bool
|
||||
expectedTTL uint32
|
||||
}{
|
||||
{
|
||||
name: "valid_config_default_ttl",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646
|
||||
token test-token
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedTTL: uint32(defaultTTL),
|
||||
},
|
||||
{
|
||||
name: "valid_config_custom_ttl",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646
|
||||
token test-token
|
||||
ttl 60
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedTTL: 60,
|
||||
},
|
||||
{
|
||||
name: "invalid_ttl_negative",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646
|
||||
token test-token
|
||||
ttl -1
|
||||
}`,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_ttl_too_large",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646
|
||||
token test-token
|
||||
ttl 3601
|
||||
}`,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_property",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646
|
||||
token test-token
|
||||
invalid_property
|
||||
}`,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "multiple_addresses",
|
||||
config: `
|
||||
nomad service.nomad {
|
||||
address http://127.0.0.1:4646 http://127.0.0.2:4646
|
||||
token test-token
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedTTL: uint32(defaultTTL),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", tt.config)
|
||||
n := &Nomad{
|
||||
ttl: uint32(defaultTTL),
|
||||
clients: make([]*nomad.Client, 0),
|
||||
current: -1,
|
||||
}
|
||||
|
||||
err := parse(c, n)
|
||||
if tt.shouldErr && err == nil {
|
||||
t.Fatalf("Test %s: expected error but got none", tt.name)
|
||||
}
|
||||
if !tt.shouldErr && err != nil {
|
||||
t.Fatalf("Test %s: expected no error but got: %v", tt.name, err)
|
||||
}
|
||||
if tt.shouldErr {
|
||||
return
|
||||
}
|
||||
|
||||
if n.ttl != tt.expectedTTL {
|
||||
t.Errorf("Test %s: expected TTL %d, got %d", tt.name, tt.expectedTTL, n.ttl)
|
||||
}
|
||||
|
||||
if len(n.clients) == 0 {
|
||||
t.Errorf("Test %s: expected at least one client to be created", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user