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>
This commit is contained in:
Sven Nebel 2021-07-14 08:25:30 +01:00 committed by GitHub
parent 936b483a3a
commit 21f1207afe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 573 additions and 6 deletions

View file

@ -30,6 +30,7 @@ go.mod @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip
/plugin/etcd/ @miekg @nitisht
/plugin/file/ @miekg @yongtang @stp-ip
/plugin/forward/ @johnbelamaric @miekg @rdrozhdzh
/plugin/geoip/ @miekg @snebel29
/plugin/grpc/ @inigohu @miekg @zouyee
/plugin/health/ @fastest963 @miekg @zouyee
/plugin/hosts/ @johnbelamaric @pmoroney

View file

@ -11,6 +11,7 @@ package dnsserver
// care what plugin above them are doing.
var Directives = []string{
"metadata",
"geoip",
"cancel",
"tls",
"reload",

View file

@ -25,6 +25,7 @@ import (
_ "github.com/coredns/coredns/plugin/etcd"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/geoip"
_ "github.com/coredns/coredns/plugin/grpc"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"

1
go.mod
View file

@ -23,6 +23,7 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5
github.com/openzipkin/zipkin-go v0.2.2
github.com/oschwald/geoip2-golang v1.5.0
github.com/philhofer/fwd v1.1.1 // indirect
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0

5
go.sum
View file

@ -342,6 +342,10 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw=
github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
@ -558,6 +562,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View file

@ -20,6 +20,7 @@
# log:log
metadata:metadata
geoip:geoip
cancel:cancel
tls:tls
reload:reload

73
plugin/geoip/README.md Normal file
View file

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

58
plugin/geoip/city.go Normal file
View file

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

95
plugin/geoip/geoip.go Normal file
View file

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

View file

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

53
plugin/geoip/setup.go Normal file
View file

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

109
plugin/geoip/setup_test.go Normal file
View file

@ -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")
}
}

BIN
plugin/geoip/testdata/GeoLite2-City.mmdb vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

112
plugin/geoip/testdata/README.md vendored Normal file
View file

@ -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)
}
}
```

View file

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

View file

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