diff options
author | 2021-07-14 08:25:30 +0100 | |
---|---|---|
committer | 2021-07-14 09:25:30 +0200 | |
commit | 21f1207afee6915c14e1109834e3fc0dfed9f420 (patch) | |
tree | 19423b6bf9a4ed6b4b43576eb31547441bab07a3 /plugin | |
parent | 936b483a3afdb532180dd6da6fa3c686c5ca9ee9 (diff) | |
download | coredns-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.md | 73 | ||||
-rw-r--r-- | plugin/geoip/city.go | 58 | ||||
-rw-r--r-- | plugin/geoip/geoip.go | 95 | ||||
-rw-r--r-- | plugin/geoip/geoip_test.go | 61 | ||||
-rw-r--r-- | plugin/geoip/setup.go | 53 | ||||
-rw-r--r-- | plugin/geoip/setup_test.go | 109 | ||||
-rw-r--r-- | plugin/geoip/testdata/GeoLite2-City.mmdb | bin | 0 -> 3281 bytes | |||
-rw-r--r-- | plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb | bin | 0 -> 3280 bytes | |||
-rw-r--r-- | plugin/geoip/testdata/README.md | 112 | ||||
-rw-r--r-- | plugin/metadata/metadata_test.go | 2 | ||||
-rw-r--r-- | plugin/metadata/provider.go | 6 |
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 Binary files differnew file mode 100644 index 000000000..cd79ed914 --- /dev/null +++ b/plugin/geoip/testdata/GeoLite2-City.mmdb diff --git a/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb Binary files differnew file mode 100644 index 000000000..23efbf396 --- /dev/null +++ b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb 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 } |