diff --git a/README.md b/README.md index df37b8c..c9fa1cc 100644 --- a/README.md +++ b/README.md @@ -72,43 +72,42 @@ func main() { } ``` -## Supported Databases - -| | Postgres | CockroachDB | Redis | KeyDB | Etcd | Memory | -|------------------------------|----------|-------------|---------|---------|------------|------------| -| Production ready | Y | Y | Y | Y | Y | N | -| geo redundant setup possible | N | Y | N | Y | N | N | -| AcquireIP/sec | ~100/s | ~60/s | ~1400/s | ~1400/s | ~110/s | >200.000/s | -| AcquireChildPrefix/sec | ~40/s | ~35/s | ~1000/s | ~1000/s | ~70/s | >100.000/s | - -Test were run on a Intel(R) Core(TM) i5-6600 CPU @ 3.30GHz - -## Performance - -```bash -BenchmarkNewPrefix/Memory-4 464994 2675 ns/op 1728 B/op 27 allocs/op -BenchmarkNewPrefix/Postgres-4 126 11775448 ns/op 6259 B/op 144 allocs/op -BenchmarkNewPrefix/Cockroach-4 100 25558820 ns/op 6250 B/op 144 allocs/op -BenchmarkNewPrefix/Redis-4 3854 308122 ns/op 3930 B/op 78 allocs/op -BenchmarkNewPrefix/KeyDB-4 3907 307655 ns/op 3930 B/op 78 allocs/op -BenchmarkAcquireIP/Memory-4 229524 4508 ns/op 2680 B/op 56 allocs/op -BenchmarkAcquireIP/Postgres-4 98 14918027 ns/op 10684 B/op 263 allocs/op -BenchmarkAcquireIP/Cockroach-4 51 19688920 ns/op 10728 B/op 264 allocs/op -BenchmarkAcquireIP/Redis-4 1734 695545 ns/op 12113 B/op 268 allocs/op -BenchmarkAcquireIP/KeyDB-4 1476 751854 ns/op 12110 B/op 268 allocs/op -BenchmarkAcquireChildPrefix/Memory-4 128704 8453 ns/op 5201 B/op 94 allocs/op -BenchmarkAcquireChildPrefix/Postgres-4 70 21220704 ns/op 15663 B/op 378 allocs/op -BenchmarkAcquireChildPrefix/Cockroach-4 32 37638608 ns/op 15774 B/op 381 allocs/op -BenchmarkAcquireChildPrefix/Redis-4 1280 925054 ns/op 16016 B/op 349 allocs/op -BenchmarkAcquireChildPrefix/KeyDB-4 1143 953056 ns/op 16018 B/op 349 allocs/op -BenchmarkPrefixOverlapping-4 4306106 274.4 ns/op 0 B/op 0 allocs/op -``` +## Supported Databases & Performance + +| Database | Acquire Child Prefix | Acquire IP | New Prefix | Prefix Overlap | Production-Ready | Geo-Redundant | +|:-------------|----------------------:|------------:|-------------:|----------------:|:-----------------|:-----------------------| +| In-Memory | 106,861/sec | 196,687/sec | 330,578/sec | 248/sec | N | N | +| KeyDB | 777/sec | 975/sec | 2,271/sec | | Y | Y | +| Redis | 773/sec | 958/sec | 2,349/sec | | Y | N | +| MongoDB | 415/sec | 682/sec | 772/sec | | Y | Y | +| Etcd | 258/sec | 368/sec | 533/sec | | Y | N | +| Postgres | 203/sec | 331/sec | 472/sec | | Y | N | +| CockroachDB | 40/sec | 37/sec | 46/sec | | Y | Y | + +The benchmarks above were performed using: + * cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz + * postgres:14-alpine + * cockroach:v22.1.0 + * redis:7.0-alpine + * keydb:alpine_x86_64_v6.2.2 + * etcd:v3.5.4 + * mongodb:5.0.9-focal + +### Database Version Compatability +| Database | Details | +|-------------|-----------------------------------------------------------------------------------------------| +| KeyDB | | +| Redis | | +| MongoDB | https://www.mongodb.com/docs/drivers/go/current/compatibility/#std-label-golang-compatibility | +| Etcd | | +| Postgres | | +| CockroachDB | | ## Testing individual Backends It is possible to test a individual backend only to speed up development roundtrip. -`backend` can be one of `Memory`, `Postgres`, `Cockroach`, `Etcd` and `Redis`. +`backend` can be one of `Memory`, `Postgres`, `Cockroach`, `Etcd`, `Redis`, and `MongoDB`. ```bash BACKEND=backend make test diff --git a/go.mod b/go.mod index d38c956..8cb46b7 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/stretchr/testify v1.7.2 github.com/testcontainers/testcontainers-go v0.13.0 go.etcd.io/etcd/client/v3 v3.5.4 + go.mongodb.org/mongo-driver v1.10.0 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f inet.af/netaddr v0.0.0-20220617031823-097006376321 ) @@ -33,11 +34,14 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/klauspost/compress v1.13.6 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/moby/sys/mount v0.3.2 // indirect github.com/moby/sys/mountinfo v0.6.1 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect @@ -45,6 +49,10 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.etcd.io/etcd/api/v3 v3.5.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect go.opencensus.io v0.23.0 // indirect @@ -53,6 +61,7 @@ require ( go.uber.org/zap v1.17.0 // indirect go4.org/intern v0.0.0-20220301175310-a089fc204883 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index f924098..a3c969b 100644 --- a/go.sum +++ b/go.sum @@ -382,6 +382,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -470,6 +472,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -529,6 +533,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= @@ -693,6 +699,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/testcontainers/testcontainers-go v0.13.0 h1:OUujSlEGsXVo/ykPVZk3KanBNGN0TYb/7oKIPVn15JA= github.com/testcontainers/testcontainers-go v0.13.0/go.mod h1:z1abufU633Eb/FmSBTzV6ntZAC1eZBYPtaFsn4nPuDk= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -708,11 +716,19 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17 github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -731,6 +747,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7H go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -769,6 +787,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -843,6 +863,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU= golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/mongodb.go b/mongodb.go new file mode 100644 index 0000000..126c7f3 --- /dev/null +++ b/mongodb.go @@ -0,0 +1,194 @@ +package ipam + +import ( + "context" + "errors" + "fmt" + "sync" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const dbIndex = `prefix.cidr` +const versionKey = `version` + +type MongoConfig struct { + DatabaseName string + CollectionName string + MongoClientOptions *options.ClientOptions +} + +type mongodb struct { + c *mongo.Collection + lock sync.RWMutex +} + +func NewMongo(ctx context.Context, config MongoConfig) (Storage, error) { + return newMongo(ctx, config) +} + +func newMongo(ctx context.Context, config MongoConfig) (*mongodb, error) { + m, err := mongo.NewClient(config.MongoClientOptions) + if err != nil { + return nil, err + } + err = m.Connect(ctx) + if err != nil { + return nil, err + } + + err = m.Ping(ctx, nil) + if err != nil { + return nil, err + } + + c := m.Database(config.DatabaseName).Collection(config.CollectionName) + + _, err = c.Indexes().CreateMany(context.TODO(), []mongo.IndexModel{{ + Keys: bson.M{dbIndex: 1}, + Options: options.Index().SetUnique(true), + }}) + if err != nil { + return nil, err + } + return &mongodb{c, sync.RWMutex{}}, nil +} + +func (m *mongodb) CreatePrefix(prefix Prefix) (Prefix, error) { + m.lock.Lock() + defer m.lock.Unlock() + + f := bson.D{{Key: dbIndex, Value: prefix.Cidr}} + r := m.c.FindOne(context.TODO(), f) + + // ErrNoDocuments should be returned if the prefix does not exist + if r.Err() == nil { + return Prefix{}, fmt.Errorf("prefix already exists:%s", prefix.Cidr) + } else if r.Err() != nil && !errors.Is(r.Err(), mongo.ErrNoDocuments) { // unrelated to ErrNoDocuments. + return Prefix{}, fmt.Errorf("unable to insert prefix:%s, error:%w", prefix.Cidr, r.Err()) + } // ErrNoDocuments should pass through this block + + _, err := m.c.InsertOne(ctx, prefix.toPrefixJSON()) + if err != nil { + return Prefix{}, fmt.Errorf("unable to insert prefix:%s, error:%w", prefix.Cidr, err) + } + + return prefix, nil +} + +func (m *mongodb) ReadPrefix(prefix string) (Prefix, error) { + m.lock.Lock() + defer m.lock.Unlock() + + f := bson.D{{Key: dbIndex, Value: prefix}} + r := m.c.FindOne(context.TODO(), f) + + // ErrNoDocuments should be returned if the prefix does not exist + if r.Err() != nil && errors.Is(r.Err(), mongo.ErrNoDocuments) { + return Prefix{}, fmt.Errorf(`prefix not found:%s, error:%w`, prefix, r.Err()) + } else if r.Err() != nil { + return Prefix{}, fmt.Errorf(`error while trying to find prefix:%s, error:%w`, prefix, r.Err()) + } + + j := prefixJSON{} + err := r.Decode(&j) + if err != nil { + return Prefix{}, fmt.Errorf("unable to read prefix:%w", err) + } + return j.toPrefix(), nil +} + +func (m *mongodb) DeleteAllPrefixes() error { + m.lock.Lock() + defer m.lock.Unlock() + + f := bson.D{{}} // match all documents + _, err := m.c.DeleteMany(context.TODO(), f) + if err != nil { + return fmt.Errorf(`error deleting all prefixes: %w`, err) + } + return nil +} + +func (m *mongodb) ReadAllPrefixes() (Prefixes, error) { + m.lock.Lock() + defer m.lock.Unlock() + + f := bson.D{{}} // match all documents + c, err := m.c.Find(context.TODO(), f) + if err != nil { + return nil, fmt.Errorf(`error reading all prefixes: %w`, err) + } + var r []prefixJSON + if err := c.All(context.TODO(), &r); err != nil { + return nil, fmt.Errorf(`error reading all prefixes: %w`, err) + } + + var s = make([]Prefix, len(r)) + for i, v := range r { + s[i] = v.toPrefix() + } + + return s, nil +} + +func (m *mongodb) ReadAllPrefixCidrs() ([]string, error) { + p, err := m.ReadAllPrefixes() + if err != nil { + return nil, err + } + var s = make([]string, len(p)) + for i, v := range p { + s[i] = v.Cidr + } + return s, nil +} + +func (m *mongodb) UpdatePrefix(prefix Prefix) (Prefix, error) { + m.lock.Lock() + defer m.lock.Unlock() + + oldVersion := prefix.version + prefix.version = oldVersion + 1 + + f := bson.D{{Key: dbIndex, Value: prefix.Cidr}, {Key: versionKey, Value: oldVersion}} + + o := options.Replace().SetUpsert(false) + r, err := m.c.ReplaceOne(context.TODO(), f, prefix.toPrefixJSON(), o) + if err != nil { + return Prefix{}, fmt.Errorf("unable to update prefix:%s, error: %w", prefix.Cidr, err) + } + if r.MatchedCount == 0 { + return Prefix{}, fmt.Errorf("%w: unable to update prefix:%s", ErrOptimisticLockError, prefix.Cidr) + } + if r.ModifiedCount == 0 { + return Prefix{}, fmt.Errorf("%w: update did not effect any document:%s", + ErrOptimisticLockError, prefix.Cidr) + } + + return prefix, nil +} + +func (m *mongodb) DeletePrefix(prefix Prefix) (Prefix, error) { + m.lock.Lock() + defer m.lock.Unlock() + + f := bson.D{{Key: dbIndex, Value: prefix.Cidr}} + r := m.c.FindOneAndDelete(context.TODO(), f) + + // ErrNoDocuments should be returned if the prefix does not exist + if r.Err() != nil && errors.Is(r.Err(), mongo.ErrNoDocuments) { + return Prefix{}, fmt.Errorf(`prefix not found:%s, error:%w`, prefix.Cidr, r.Err()) + } else if r.Err() != nil { + return Prefix{}, fmt.Errorf(`error while trying to find prefix:%s, error:%w`, prefix.Cidr, r.Err()) + } + + j := prefixJSON{} + err := r.Decode(&j) + if err != nil { + return Prefix{}, fmt.Errorf("unable to read prefix:%w", err) + } + return j.toPrefix(), nil +} diff --git a/testing_test.go b/testing_test.go index 1c9a6c9..37f7f7f 100644 --- a/testing_test.go +++ b/testing_test.go @@ -10,6 +10,7 @@ import ( "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" clientv3 "go.etcd.io/etcd/client/v3" + "go.mongodb.org/mongo-driver/mongo/options" ) var ( @@ -27,6 +28,9 @@ var ( etcdContainer testcontainers.Container etcdVersion string etcdOnce sync.Once + mdbOnce sync.Once + mdbContainer testcontainers.Container + mdbVersion string backend string ) @@ -53,9 +57,13 @@ func TestMain(m *testing.M) { if etcdVersion == "" { etcdVersion = "v3.5.4" } + mdbVersion = os.Getenv("MONGODB_VERSION") + if mdbVersion == "" { + mdbVersion = "5.0.9-focal" + } backend = os.Getenv("BACKEND") if backend == "" { - fmt.Printf("Using postgres:%s cockroach:%s redis:%s keydb:%s, etcd:%s\n", pgVersion, cockroachVersion, redisVersion, keyDBVersion, etcdVersion) + fmt.Printf("Using postgres:%s cockroach:%s redis:%s keydb:%s, etcd:%s mongodb:%s\n", pgVersion, cockroachVersion, redisVersion, keyDBVersion, etcdVersion, mdbVersion) } else { fmt.Printf("only test %s\n", backend) } @@ -204,6 +212,59 @@ func startEtcd() (container testcontainers.Container, s *etcd, err error) { return etcdContainer, db, nil } +func startMongodb() (container testcontainers.Container, s *mongodb, err error) { + ctx := context.Background() + + mdbOnce.Do(func() { + var err error + req := testcontainers.ContainerRequest{ + Image: `mongo:` + mdbVersion, + ExposedPorts: []string{`27017/tcp`}, + Env: map[string]string{ + `MONGO_INITDB_ROOT_USERNAME`: `testuser`, + `MONGO_INITDB_ROOT_PASSWORD`: `testuser`, + }, + WaitingFor: wait.ForAll( + wait.ForLog(`Waiting for connections`), + wait.ForListeningPort(`27017/tcp`), + ), + Cmd: []string{`mongod`}, + } + mdbContainer, err = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + panic(err.Error()) + } + }) + ip, err := mdbContainer.Host(ctx) + if err != nil { + return mdbContainer, nil, err + } + port, err := mdbContainer.MappedPort(ctx, `27017`) + if err != nil { + return mdbContainer, nil, err + } + + opts := options.Client() + opts.ApplyURI(fmt.Sprintf(`mongodb://%s:%s`, ip, port.Port())) + opts.Auth = &options.Credential{ + AuthMechanism: `SCRAM-SHA-1`, + Username: `testuser`, + Password: `testuser`, + } + + c := MongoConfig{ + DatabaseName: `go-ipam`, + CollectionName: `prefixes`, + MongoClientOptions: opts, + } + db, err := newMongo(ctx, c) + + return mdbContainer, db, err +} + func startKeyDB() (container testcontainers.Container, s *redis, err error) { ctx := context.Background() redisOnce.Do(func() { @@ -267,6 +328,11 @@ type kvEtcdStorage struct { c testcontainers.Container } +type docStorage struct { + *mongodb + c testcontainers.Container +} + func newPostgresWithCleanup() (*extendedSQL, error) { c, s, err := startPostgres() if err != nil { @@ -334,6 +400,19 @@ func newKeyDBWithCleanup() (*kvStorage, error) { return kv, nil } +func newMongodbWithCleanup() (*docStorage, error) { + c, s, err := startMongodb() + if err != nil { + return nil, err + } + + x := &docStorage{ + mongodb: s, + c: c, + } + return x, nil +} + // cleanup database before test func (e *extendedSQL) cleanup() error { tx := e.sql.db.MustBegin() @@ -372,6 +451,12 @@ func (sql *sql) cleanup() error { return tx.Commit() } +func (ds *docStorage) cleanup() error { + ds.mongodb.lock.Lock() + defer ds.mongodb.lock.Unlock() + return ds.mongodb.c.Drop(context.TODO()) +} + type benchMethod func(b *testing.B, ipam *ipamer) func benchWithBackends(b *testing.B, fn benchMethod) { @@ -545,5 +630,18 @@ func storageProviders() []storageProvider { return nil }, }, + { + name: "MongoDB", + provide: func() Storage { + storage, err := newMongodbWithCleanup() + if err != nil { + panic(fmt.Sprintf(`error getting mongodb storage, error: %s`, err)) + } + return storage + }, + providesql: func() *sql { + return nil + }, + }, } }