aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar Sven Nebel <nebel.sven@gmail.com> 2021-07-14 08:25:30 +0100
committerGravatar GitHub <noreply@github.com> 2021-07-14 09:25:30 +0200
commit21f1207afee6915c14e1109834e3fc0dfed9f420 (patch)
tree19423b6bf9a4ed6b4b43576eb31547441bab07a3 /plugin
parent936b483a3afdb532180dd6da6fa3c686c5ca9ee9 (diff)
downloadcoredns-21f1207afee6915c14e1109834e3fc0dfed9f420.tar.gz
coredns-21f1207afee6915c14e1109834e3fc0dfed9f420.tar.zst
coredns-21f1207afee6915c14e1109834e3fc0dfed9f420.zip
Create geoip plugin (#4688)
* Create geoip plugin Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Move DBFILE bullet below example Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove plugin name test case Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove languages option Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update free database link Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove last language bits Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Use 127.0.0.1 as probing IP Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Use relative path for fixtures dir Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Set names with default string zero value Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove unused db types Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove non city databases in testdata Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove create databases main Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Fix metadata label format test case Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Fix import path block Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * go fmt after changes Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Tidy up go.mod and go.sum Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Add plugin to CODEOWNERS Signed-off-by: Sven Nebel <nebel.sven@gmail.com> Co-authored-by: Miek Gieben <miek@miek.nl>
Diffstat (limited to 'plugin')
-rw-r--r--plugin/geoip/README.md73
-rw-r--r--plugin/geoip/city.go58
-rw-r--r--plugin/geoip/geoip.go95
-rw-r--r--plugin/geoip/geoip_test.go61
-rw-r--r--plugin/geoip/setup.go53
-rw-r--r--plugin/geoip/setup_test.go109
-rw-r--r--plugin/geoip/testdata/GeoLite2-City.mmdbbin0 -> 3281 bytes
-rw-r--r--plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdbbin0 -> 3280 bytes
-rw-r--r--plugin/geoip/testdata/README.md112
-rw-r--r--plugin/metadata/metadata_test.go2
-rw-r--r--plugin/metadata/provider.go6
11 files changed, 563 insertions, 6 deletions
diff --git a/plugin/geoip/README.md b/plugin/geoip/README.md
new file mode 100644
index 000000000..b666518f7
--- /dev/null
+++ b/plugin/geoip/README.md
@@ -0,0 +1,73 @@
+# geoip
+
+## Name
+*geoip* - Lookup maxmind geoip2 databases using the client IP, then add associated geoip data to the context request.
+
+## Description
+The *geoip* plugin add geo location data associated with the client IP, it allows you to configure a [geoIP2 maxmind database](https://dev.maxmind.com/geoip/docs/databases) to add the geo location data associated with the IP address.
+
+The data is added leveraging the *metadata* plugin, values can then be retrieved using it as well, for example:
+
+```go
+import (
+ "strconv"
+ "github.com/coredns/coredns/plugin/metadata"
+)
+// ...
+if getLongitude := metadata.ValueFunc(ctx, "geoip/longitude"); getLongitude != nil {
+ if longitude, err := strconv.ParseFloat(getLongitude(), 64); err == nil {
+ // Do something useful with longitude.
+ }
+} else {
+ // The metadata label geoip/longitude for some reason, was not set.
+}
+// ...
+```
+
+## Databases
+The supported databases use city schema such as `City` and `Enterprise`. Other databases types with different schemas are not supported yet.
+
+You can download a [free and public City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).
+
+## Syntax
+```txt
+geoip [DBFILE]
+```
+* **DBFILE** the mmdb database file path.
+
+## Examples
+The following configuration configures the `City` database.
+```txt
+. {
+ geoip /opt/geoip2/db/GeoLite2-City.mmdb
+ metadata # Note that metadata plugin must be enabled as well.
+}
+```
+
+## Metadatada Labels
+A limited set of fields will be exported as labels, all values are stored using strings **regardless of their underlying value type**, and therefore you may have to convert it back to its original type, note that numeric values are always represented in base 10.
+
+| Label | Type | Example | Description
+| :----------------------------------- | :-------- | :-------------- | :------------------
+| `geoip/city/name` | `string` | `Cambridge` | Then city name in English language.
+| `geoip/country/code` | `string` | `GB` | Country [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) code.
+| `geoip/country/name` | `string` | `United Kingdom` | The country name in English language.
+| `geoip/country/is_in_european_union` | `bool` | `false` | Either `true` or `false`.
+| `geoip/continent/code` | `string` | `EU` | See [Continent codes](#ContinentCodes).
+| `geoip/continent/name` | `string` | `Europe` | The continent name in English language.
+| `geoip/latitude` | `float64` | `52.2242` | Base 10, max available precision.
+| `geoip/longitude` | `float64` | `0.1315` | Base 10, max available precision.
+| `geoip/timezone` | `string` | `Europe/London` | The timezone.
+| `geoip/postalcode` | `string` | `CB4` | The postal code.
+
+## Continent Codes
+
+| Value | Continent (EN) |
+| :---- | :------------- |
+| AF | Africa |
+| AN | Antarctica |
+| AS | Asia |
+| EU | Europe |
+| NA | North America |
+| OC | Oceania |
+| SA | South America |
diff --git a/plugin/geoip/city.go b/plugin/geoip/city.go
new file mode 100644
index 000000000..4cfd254a6
--- /dev/null
+++ b/plugin/geoip/city.go
@@ -0,0 +1,58 @@
+package geoip
+
+import (
+ "context"
+ "strconv"
+
+ "github.com/coredns/coredns/plugin/metadata"
+
+ "github.com/oschwald/geoip2-golang"
+)
+
+const defaultLang = "en"
+
+func (g GeoIP) setCityMetadata(ctx context.Context, data *geoip2.City) {
+ // Set labels for city, country and continent names.
+ cityName := data.City.Names[defaultLang]
+ metadata.SetValueFunc(ctx, pluginName+"/city/name", func() string {
+ return cityName
+ })
+ countryName := data.Country.Names[defaultLang]
+ metadata.SetValueFunc(ctx, pluginName+"/country/name", func() string {
+ return countryName
+ })
+ continentName := data.Continent.Names[defaultLang]
+ metadata.SetValueFunc(ctx, pluginName+"/continent/name", func() string {
+ return continentName
+ })
+
+ countryCode := data.Country.IsoCode
+ metadata.SetValueFunc(ctx, pluginName+"/country/code", func() string {
+ return countryCode
+ })
+ isInEurope := strconv.FormatBool(data.Country.IsInEuropeanUnion)
+ metadata.SetValueFunc(ctx, pluginName+"/country/is_in_european_union", func() string {
+ return isInEurope
+ })
+ continentCode := data.Continent.Code
+ metadata.SetValueFunc(ctx, pluginName+"/continent/code", func() string {
+ return continentCode
+ })
+
+ latitude := strconv.FormatFloat(float64(data.Location.Latitude), 'f', -1, 64)
+ metadata.SetValueFunc(ctx, pluginName+"/latitude", func() string {
+ return latitude
+ })
+ longitude := strconv.FormatFloat(float64(data.Location.Longitude), 'f', -1, 64)
+ metadata.SetValueFunc(ctx, pluginName+"/longitude", func() string {
+ return longitude
+ })
+ timeZone := data.Location.TimeZone
+ metadata.SetValueFunc(ctx, pluginName+"/timezone", func() string {
+ return timeZone
+ })
+ postalCode := data.Postal.Code
+ metadata.SetValueFunc(ctx, pluginName+"/postalcode", func() string {
+ return postalCode
+ })
+}
diff --git a/plugin/geoip/geoip.go b/plugin/geoip/geoip.go
new file mode 100644
index 000000000..674157716
--- /dev/null
+++ b/plugin/geoip/geoip.go
@@ -0,0 +1,95 @@
+// Package geoip implements a max mind database plugin.
+package geoip
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "path/filepath"
+
+ "github.com/coredns/coredns/plugin"
+ clog "github.com/coredns/coredns/plugin/pkg/log"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "github.com/oschwald/geoip2-golang"
+)
+
+var log = clog.NewWithPlugin(pluginName)
+
+// GeoIP is a plugin that add geo location data to the request context by looking up a maxmind
+// geoIP2 database, and which data can be later consumed by other middlewares.
+type GeoIP struct {
+ Next plugin.Handler
+ db db
+}
+
+type db struct {
+ *geoip2.Reader
+ // provides defines the schemas that can be obtained by querying this database, by using
+ // bitwise operations.
+ provides int
+}
+
+const (
+ city = 1 << iota
+)
+
+var probingIP = net.ParseIP("127.0.0.1")
+
+func newGeoIP(dbPath string) (*GeoIP, error) {
+ reader, err := geoip2.Open(dbPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open database file: %v", err)
+ }
+ db := db{Reader: reader}
+ schemas := []struct {
+ provides int
+ name string
+ validate func() error
+ }{
+ {name: "city", provides: city, validate: func() error { _, err := reader.City(probingIP); return err }},
+ }
+ // Query the database to figure out the database type.
+ for _, schema := range schemas {
+ if err := schema.validate(); err != nil {
+ // If we get an InvalidMethodError then we know this database does not provide that schema.
+ if _, ok := err.(geoip2.InvalidMethodError); !ok {
+ return nil, fmt.Errorf("unexpected failure looking up database %q schema %q: %v", filepath.Base(dbPath), schema.name, err)
+ }
+ } else {
+ db.provides = db.provides | schema.provides
+ }
+ }
+
+ if db.provides&city == 0 {
+ return nil, fmt.Errorf("database does not provide city schema")
+ }
+
+ return &GeoIP{db: db}, nil
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (g GeoIP) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ return plugin.NextOrFailure(pluginName, g.Next, ctx, w, r)
+}
+
+// Metadata implements the metadata.Provider Interface in the metadata plugin, and is used to store
+// the data associated with the source IP of every request.
+func (g GeoIP) Metadata(ctx context.Context, state request.Request) context.Context {
+ srcIP := net.ParseIP(state.IP())
+
+ switch {
+ case g.db.provides&city == city:
+ data, err := g.db.City(srcIP)
+ if err != nil {
+ log.Debugf("Setting up metadata failed due to database lookup error: %v", err)
+ return ctx
+ }
+ g.setCityMetadata(ctx, data)
+ }
+ return ctx
+}
+
+// Name implements the Handler interface.
+func (g GeoIP) Name() string { return pluginName }
diff --git a/plugin/geoip/geoip_test.go b/plugin/geoip/geoip_test.go
new file mode 100644
index 000000000..99213138b
--- /dev/null
+++ b/plugin/geoip/geoip_test.go
@@ -0,0 +1,61 @@
+package geoip
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/metadata"
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+)
+
+func TestMetadata(t *testing.T) {
+
+ tests := []struct {
+ dbPath string
+ label string
+ expectedValue string
+ }{
+ {cityDBPath, "geoip/city/name", "Cambridge"},
+
+ {cityDBPath, "geoip/country/code", "GB"},
+ {cityDBPath, "geoip/country/name", "United Kingdom"},
+ // is_in_european_union is set to true only to work around bool zero value, and test is really being set.
+ {cityDBPath, "geoip/country/is_in_european_union", "true"},
+
+ {cityDBPath, "geoip/continent/code", "EU"},
+ {cityDBPath, "geoip/continent/name", "Europe"},
+
+ {cityDBPath, "geoip/latitude", "52.2242"},
+ {cityDBPath, "geoip/longitude", "0.1315"},
+ {cityDBPath, "geoip/timezone", "Europe/London"},
+ {cityDBPath, "geoip/postalcode", "CB4"},
+ }
+
+ for i, _test := range tests {
+ geoIP, err := newGeoIP(_test.dbPath)
+ if err != nil {
+ t.Fatalf("Test %d: unable to create geoIP plugin: %v", i, err)
+ }
+ state := request.Request{
+ W: &test.ResponseWriter{RemoteIP: "81.2.69.142"}, // This IP should be be part of the CDIR address range used to create the database fixtures.
+ }
+ ctx := metadata.ContextWithMetadata(context.Background())
+ rCtx := geoIP.Metadata(ctx, state)
+ if fmt.Sprintf("%p", ctx) != fmt.Sprintf("%p", rCtx) {
+ t.Errorf("Test %d: returned context is expected to be the same one passed in the Metadata function", i)
+ }
+
+ fn := metadata.ValueFunc(ctx, _test.label)
+ if fn == nil {
+ t.Errorf("Test %d: label %q not set in metadata plugin context", i, _test.label)
+ continue
+ }
+ value := fn()
+ if value != _test.expectedValue {
+ t.Errorf("Test %d: expected value for label %q should be %q, got %q instead",
+ i, _test.label, _test.expectedValue, value)
+ }
+ }
+}
diff --git a/plugin/geoip/setup.go b/plugin/geoip/setup.go
new file mode 100644
index 000000000..6883bbe2d
--- /dev/null
+++ b/plugin/geoip/setup.go
@@ -0,0 +1,53 @@
+package geoip
+
+import (
+ "github.com/coredns/caddy"
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+)
+
+const pluginName = "geoip"
+
+func init() { plugin.Register(pluginName, setup) }
+
+func setup(c *caddy.Controller) error {
+ geoip, err := geoipParse(c)
+ if err != nil {
+ return plugin.Error(pluginName, err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ geoip.Next = next
+ return geoip
+ })
+
+ return nil
+}
+
+func geoipParse(c *caddy.Controller) (*GeoIP, error) {
+ var dbPath string
+
+ for c.Next() {
+ if !c.NextArg() {
+ return nil, c.ArgErr()
+ }
+ if dbPath != "" {
+ return nil, c.Errf("configuring multiple databases is not supported")
+ }
+ dbPath = c.Val()
+ // There shouldn't be any more arguments.
+ if len(c.RemainingArgs()) != 0 {
+ return nil, c.ArgErr()
+ }
+ // The plugin should not have any config block.
+ if c.NextBlock() {
+ return nil, c.Err("unexpected config block")
+ }
+ }
+
+ geoIP, err := newGeoIP(dbPath)
+ if err != nil {
+ return geoIP, c.Err(err.Error())
+ }
+ return geoIP, nil
+}
diff --git a/plugin/geoip/setup_test.go b/plugin/geoip/setup_test.go
new file mode 100644
index 000000000..94d40adbc
--- /dev/null
+++ b/plugin/geoip/setup_test.go
@@ -0,0 +1,109 @@
+package geoip
+
+import (
+ "fmt"
+ "net"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/coredns/caddy"
+ "github.com/coredns/coredns/core/dnsserver"
+)
+
+var (
+ fixturesDir = "./testdata"
+ cityDBPath = filepath.Join(fixturesDir, "GeoLite2-City.mmdb")
+ unknownDBPath = filepath.Join(fixturesDir, "GeoLite2-UnknownDbType.mmdb")
+)
+
+func TestProbingIP(t *testing.T) {
+ if probingIP == nil {
+ t.Fatalf("Invalid probing IP: %q", probingIP)
+ }
+}
+
+func TestSetup(t *testing.T) {
+ c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath))
+ plugins := dnsserver.GetConfig(c).Plugin
+ if len(plugins) != 0 {
+ t.Fatalf("Expected zero plugins after setup, %d found", len(plugins))
+ }
+ if err := setup(c); err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+ plugins = dnsserver.GetConfig(c).Plugin
+ if len(plugins) != 1 {
+ t.Fatalf("Expected one plugin after setup, %d found", len(plugins))
+ }
+}
+
+func TestGeoIPParse(t *testing.T) {
+ c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath))
+ if err := setup(c); err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+
+ tests := []struct {
+ shouldErr bool
+ config string
+ expectedErr string
+ expectedDBType int
+ }{
+ // Valid
+ {false, fmt.Sprintf("%s %s\n", pluginName, cityDBPath), "", city},
+
+ // Invalid
+ {true, pluginName, "Wrong argument count", 0},
+ {true, fmt.Sprintf("%s %s {\n\tlanguages en fr es zh-CN\n}\n", pluginName, cityDBPath), "unexpected config block", 0},
+ {true, fmt.Sprintf("%s %s\n%s %s\n", pluginName, cityDBPath, pluginName, cityDBPath), "configuring multiple databases is not supported", 0},
+ {true, fmt.Sprintf("%s 1 2 3", pluginName), "Wrong argument count", 0},
+ {true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 0},
+ {true, fmt.Sprintf("%s /dbpath { city }", pluginName), "unexpected config block", 0},
+ {true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath: no such file or directory", 0},
+ {true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0},
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.config)
+ geoIP, err := geoipParse(c)
+
+ if test.shouldErr && err == nil {
+ t.Errorf("Test %d: expected error but found none for input %s", i, test.config)
+ }
+
+ if err != nil {
+ if !test.shouldErr {
+ t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.config, err)
+ }
+
+ if !strings.Contains(err.Error(), test.expectedErr) {
+ t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.config)
+ }
+ continue
+ }
+
+ if geoIP.db.Reader == nil {
+ t.Errorf("Test %d: after parsing database reader should be initialized", i)
+ }
+
+ if geoIP.db.provides&test.expectedDBType == 0 {
+ t.Errorf("Test %d: expected db type %d not found, database file provides %d", i, test.expectedDBType, geoIP.db.provides)
+ }
+ }
+
+ // Set nil probingIP to test unexpected validate error()
+ defer func(ip net.IP) { probingIP = ip }(probingIP)
+ probingIP = nil
+
+ c = caddy.NewTestController("dns", fmt.Sprintf("%s %s\n", pluginName, cityDBPath))
+ _, err := geoipParse(c)
+ if err != nil {
+ expectedErr := "unexpected failure looking up database"
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Errorf("expected error to contain: %s", expectedErr)
+ }
+ } else {
+ t.Errorf("with a nil probingIP test is expected to fail")
+ }
+}
diff --git a/plugin/geoip/testdata/GeoLite2-City.mmdb b/plugin/geoip/testdata/GeoLite2-City.mmdb
new file mode 100644
index 000000000..cd79ed914
--- /dev/null
+++ b/plugin/geoip/testdata/GeoLite2-City.mmdb
Binary files differ
diff --git a/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb
new file mode 100644
index 000000000..23efbf396
--- /dev/null
+++ b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb
Binary files differ
diff --git a/plugin/geoip/testdata/README.md b/plugin/geoip/testdata/README.md
new file mode 100644
index 000000000..2f6f884c9
--- /dev/null
+++ b/plugin/geoip/testdata/README.md
@@ -0,0 +1,112 @@
+# testdata
+This directory contains mmdb database files used during the testing of this plugin.
+
+# Create mmdb database files
+If you need to change them to add a new value, or field the best is to recreate them, the code snipped used to create them initially is provided next.
+
+```golang
+package main
+
+import (
+ "log"
+ "net"
+ "os"
+
+ "github.com/maxmind/mmdbwriter"
+ "github.com/maxmind/mmdbwriter/inserter"
+ "github.com/maxmind/mmdbwriter/mmdbtype"
+)
+
+const cdir = "81.2.69.142/32"
+
+// Create new mmdb database fixtures in this directory.
+func main() {
+ createCityDB("GeoLite2-City.mmdb", "DBIP-City-Lite")
+ // Create unkwnon database type.
+ createCityDB("GeoLite2-UnknownDbType.mmdb", "UnknownDbType")
+}
+
+func createCityDB(dbName, dbType string) {
+ // Load a database writer.
+ writer, err := mmdbwriter.New(mmdbwriter.Options{DatabaseType: dbType})
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Define and insert the new data.
+ _, ip, err := net.ParseCIDR(cdir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // TODO(snebel29): Find an alternative location in Europe Union.
+ record := mmdbtype.Map{
+ "city": mmdbtype.Map{
+ "geoname_id": mmdbtype.Uint64(2653941),
+ "names": mmdbtype.Map{
+ "en": mmdbtype.String("Cambridge"),
+ "es": mmdbtype.String("Cambridge"),
+ },
+ },
+ "continent": mmdbtype.Map{
+ "code": mmdbtype.String("EU"),
+ "geoname_id": mmdbtype.Uint64(6255148),
+ "names": mmdbtype.Map{
+ "en": mmdbtype.String("Europe"),
+ "es": mmdbtype.String("Europa"),
+ },
+ },
+ "country": mmdbtype.Map{
+ "iso_code": mmdbtype.String("GB"),
+ "geoname_id": mmdbtype.Uint64(2635167),
+ "names": mmdbtype.Map{
+ "en": mmdbtype.String("United Kingdom"),
+ "es": mmdbtype.String("Reino Unido"),
+ },
+ "is_in_european_union": mmdbtype.Bool(true),
+ },
+ "location": mmdbtype.Map{
+ "accuracy_radius": mmdbtype.Uint16(200),
+ "latitude": mmdbtype.Float64(52.2242),
+ "longitude": mmdbtype.Float64(0.1315),
+ "metro_code": mmdbtype.Uint64(0),
+ "time_zone": mmdbtype.String("Europe/London"),
+ },
+ "postal": mmdbtype.Map{
+ "code": mmdbtype.String("CB4"),
+ },
+ "registered_country": mmdbtype.Map{
+ "iso_code": mmdbtype.String("GB"),
+ "geoname_id": mmdbtype.Uint64(2635167),
+ "names": mmdbtype.Map{"en": mmdbtype.String("United Kingdom")},
+ "is_in_european_union": mmdbtype.Bool(false),
+ },
+ "subdivisions": mmdbtype.Slice{
+ mmdbtype.Map{
+ "iso_code": mmdbtype.String("ENG"),
+ "geoname_id": mmdbtype.Uint64(6269131),
+ "names": mmdbtype.Map{"en": mmdbtype.String("England")},
+ },
+ mmdbtype.Map{
+ "iso_code": mmdbtype.String("CAM"),
+ "geoname_id": mmdbtype.Uint64(2653940),
+ "names": mmdbtype.Map{"en": mmdbtype.String("Cambridgeshire")},
+ },
+ },
+ }
+
+ if err := writer.InsertFunc(ip, inserter.TopLevelMergeWith(record)); err != nil {
+ log.Fatal(err)
+ }
+
+ // Write the DB to the filesystem.
+ fh, err := os.Create(dbName)
+ if err != nil {
+ log.Fatal(err)
+ }
+ _, err = writer.WriteTo(fh)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
diff --git a/plugin/metadata/metadata_test.go b/plugin/metadata/metadata_test.go
index be20f8770..3dc507de0 100644
--- a/plugin/metadata/metadata_test.go
+++ b/plugin/metadata/metadata_test.go
@@ -72,12 +72,12 @@ func TestLabelFormat(t *testing.T) {
{"plugin/LABEL", true},
{"p/LABEL", true},
{"plugin/L", true},
+ {"PLUGIN/LABEL/SUB-LABEL", true},
// fails
{"LABEL", false},
{"plugin.LABEL", false},
{"/NO-PLUGIN-NOT-ACCEPTED", false},
{"ONLY-PLUGIN-NOT-ACCEPTED/", false},
- {"PLUGIN/LABEL/SUB-LABEL", false},
{"/", false},
{"//", false},
}
diff --git a/plugin/metadata/provider.go b/plugin/metadata/provider.go
index 309d304b7..06417cc6f 100644
--- a/plugin/metadata/provider.go
+++ b/plugin/metadata/provider.go
@@ -56,17 +56,13 @@ type Provider interface {
// Func is the type of function in the metadata, when called they return the value of the label.
type Func func() string
-// IsLabel checks that the provided name is a valid label name, i.e. two words separated by a slash.
+// IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash.
func IsLabel(label string) bool {
p := strings.Index(label, "/")
if p <= 0 || p >= len(label)-1 {
// cannot accept namespace empty nor label empty
return false
}
- if strings.LastIndex(label, "/") != p {
- // several slash in the Label
- return false
- }
return true
}