fix: upgrade coredns to v1.10.1 at 4.0 (#24619)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2026-04-08 16:26:50 +08:00
committed by GitHub
parent 05da992fba
commit f9ac278ba3
1353 changed files with 203771 additions and 125982 deletions

View File

@@ -17,7 +17,11 @@ package main
import (
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/coremain"
_ "github.com/coredns/coredns/plugin/acl"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/bufsize"
_ "github.com/coredns/coredns/plugin/cache"
_ "github.com/coredns/coredns/plugin/cancel"
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/errors"
@@ -25,12 +29,18 @@ import (
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/local"
_ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/metrics"
_ "github.com/coredns/coredns/plugin/loop"
_ "github.com/coredns/coredns/plugin/metrics" // prometheus
_ "github.com/coredns/coredns/plugin/nsid"
_ "github.com/coredns/coredns/plugin/proxy"
_ "github.com/coredns/coredns/plugin/ready"
_ "github.com/coredns/coredns/plugin/reload"
_ "github.com/coredns/coredns/plugin/rewrite"
_ "github.com/coredns/coredns/plugin/timeouts"
_ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/whoami"
_ "github.com/mholt/caddy/startupshutdown"
_ "yunion.io/x/onecloud/pkg/dns"
@@ -38,18 +48,31 @@ import (
)
var directives = []string{
"cancel",
"timeouts",
"reload",
"nsid",
"bufsize",
"bind",
"debug",
"trace",
"ready",
"health",
"prometheus",
"errors",
"log",
"local",
"chaos",
"loadbalance",
"cache",
"rewrite",
"acl",
"yunion",
"hosts",
"file",
"yunion",
"loop",
"forward",
"proxy",
"whoami",
"startup",
"shutdown",
}

79
go.mod
View File

@@ -15,7 +15,8 @@ require (
github.com/cheggaaa/pb/v3 v3.0.8
github.com/containerd/containerd v1.7.13
github.com/containerd/typeurl v1.0.2
github.com/coredns/coredns v1.3.0
github.com/coredns/caddy v1.1.1
github.com/coredns/coredns v1.10.1
github.com/coreos/go-iptables v0.6.0
github.com/creack/pty v1.1.18
github.com/docker/go-units v0.5.0
@@ -53,14 +54,14 @@ require (
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
github.com/mdlayher/packet v1.1.2
github.com/mholt/caddy v0.10.11
github.com/miekg/dns v1.1.25
github.com/miekg/dns v1.1.50
github.com/minio/minio-go v6.0.14+incompatible
github.com/mitchellh/go-wordwrap v1.0.1
github.com/moby/term v0.5.0
github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73
github.com/opencontainers/runtime-spec v1.1.0
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490
github.com/pierrec/lz4/v4 v4.1.15
github.com/pierrec/lz4/v4 v4.1.17
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.2.0
@@ -83,8 +84,8 @@ require (
github.com/xuri/excelize/v2 v2.7.1
github.com/zeebo/xxh3 v1.0.2
github.com/zexi/influxql-to-metricsql v0.1.1
go.etcd.io/etcd/api/v3 v3.5.0
go.etcd.io/etcd/client/v3 v3.5.0
go.etcd.io/etcd/api/v3 v3.5.7
go.etcd.io/etcd/client/v3 v3.5.7
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
@@ -128,19 +129,23 @@ require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-autorest/autorest v0.9.6 // indirect
github.com/Azure/go-autorest/autorest/adal v0.8.2 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 // indirect
github.com/Azure/go-autorest/autorest/date v0.2.0 // indirect
github.com/Azure/go-autorest/logger v0.1.0 // indirect
github.com/Azure/go-autorest/tracing v0.5.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/ClickHouse/clickhouse-go v1.5.4 // indirect
github.com/DataDog/dd-trace-go v0.6.1 // indirect
github.com/DataDog/zstd v1.3.4 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 // indirect
github.com/DataDog/datadog-go v4.8.2+incompatible // indirect
github.com/DataDog/datadog-go/v5 v5.0.2 // indirect
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork // indirect
github.com/DataDog/sketches-go v1.2.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/Shopify/sarama v1.20.0 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
@@ -162,8 +167,8 @@ require (
github.com/anacrolix/upnp v0.1.4 // indirect
github.com/anacrolix/utp v0.1.0 // indirect
github.com/aokoli/goutils v1.0.1 // indirect
github.com/apache/thrift v0.13.0 // indirect
github.com/aws/aws-sdk-go v1.39.0 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.44.194 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
@@ -210,18 +215,15 @@ require (
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dimchansky/utfbom v1.1.0 // indirect
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnstap/golang-dnstap v0.4.0 // indirect
github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/eapache/go-resiliency v1.1.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/euank/go-kmsg-parser v2.0.0+incompatible // indirect
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710 // indirect
github.com/farsightsec/golang-framestream v0.3.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
@@ -229,7 +231,6 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
@@ -244,6 +245,8 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gofrs/uuid v4.1.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
@@ -262,7 +265,8 @@ require (
github.com/huandu/xstrings v1.3.2 // indirect
github.com/huaweicloud/huaweicloud-sdk-go v1.0.26 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jdcloud-api/jdcloud-sdk-go v1.55.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -286,7 +290,7 @@ require (
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f // indirect
@@ -321,9 +325,11 @@ require (
github.com/opencontainers/runc v1.1.5 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect
github.com/opentracing/opentracing-go v1.0.2 // indirect
github.com/openzipkin/zipkin-go-opentracing v0.3.4 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0 // indirect
github.com/openzipkin/zipkin-go v0.4.1 // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.2.4 // indirect
@@ -348,16 +354,16 @@ require (
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
@@ -368,6 +374,7 @@ require (
github.com/tencentyun/cos-go-sdk-v5 v0.7.24 // indirect
github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.5.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
@@ -381,7 +388,7 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect
@@ -391,16 +398,22 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.17.0 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/api v0.167.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.47.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
inet.af/netaddr v0.0.0-20220617031823-097006376321 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
lukechampine.com/blake3 v1.1.6 // indirect
@@ -415,3 +428,5 @@ require (
)
replace github.com/influxdata/promql/v2 => github.com/zexi/promql/v2 v2.12.1
replace inet.af/netaddr => github.com/inetaf/netaddr v0.0.0-20211027220019-c74959edd3b6

203
go.sum
View File

@@ -58,41 +58,47 @@ github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0=
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0=
github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/DataDog/dd-trace-go v0.6.1 h1:nsZ2lohbSw1CKtfNRu3wPh1jFirv6XSz8vqNpuIYWbM=
github.com/DataDog/dd-trace-go v0.6.1/go.mod h1:SmQTTcC37XMyEm75HV0AWiZIYxDiaNhRi49zorIpW+o=
github.com/DataDog/zstd v1.3.4 h1:LAGHkXuvC6yky+C2CUG2tD7w8QlrUwpue8XwIh0X4AY=
github.com/DataDog/zstd v1.3.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 h1:Rmz52Xlc5k3WzAHzD0SCH4USCzyti7EbK4HtrHys3ME=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1/go.mod h1:VVMDDibJxYEkwcLdZBT2g8EHKpbMT4JdOhRbQ9GdjbM=
github.com/DataDog/datadog-go v4.8.2+incompatible h1:qbcKSx29aBLD+5QLvlQZlGmRMF/FfGqFLFev/1TDzRo=
github.com/DataDog/datadog-go v4.8.2+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go/v5 v5.0.2 h1:UFtEe7662/Qojxkw1d6SboAeA0CPI3naKhVASwFn+04=
github.com/DataDog/datadog-go/v5 v5.0.2/go.mod h1:ZI9JFB4ewXbw1sBnF4sxsR2k1H3xjV+PUAOUsHvKpcU=
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork h1:yBq5PrAtrM4yVeSzQ+bn050+Ysp++RKF1QmtkL4VqvU=
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork/go.mod h1:yA5JwkZsHTLuqq3zaRgUQf35DfDkpOZqgtBqHKpwrBs=
github.com/DataDog/sketches-go v1.2.1 h1:qTBzWLnZ3kM2kw39ymh6rMcnN+5VULwFs++lEYUUsro=
github.com/DataDog/sketches-go v1.2.1/go.mod h1:1xYmPLY1So10AwxV6MJV0J53XVH+WL9Ad1KetxVivVI=
github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2 h1:XGB3B0651J1uKOE1KJa1gsrV/DO1kthhk2NTDUHATgs=
github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2/go.mod h1:tiLv6VBLH4Z3KdBSe2qIKRwQDGCVQ9/F5fOKpQGvyoA=
github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
@@ -110,9 +116,6 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.20.0 h1:wAMHhl1lGRlobeoV/xOKpbqD2OQsOvY4A/vIOGroIe8=
github.com/Shopify/sarama v1.20.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
@@ -195,12 +198,12 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.35.24/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo=
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.44.194 h1:1ZDK+QDcc5oRbZGgRZSz561eR8XVizXCeGpoZKo33NU=
github.com/aws/aws-sdk-go v1.44.194/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
@@ -305,6 +308,7 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
@@ -324,8 +328,10 @@ github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY
github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
github.com/coredns/coredns v1.3.0 h1:Ubjr8ZrOpEj95/A9dqQqQAG2k5DWEFdg2ZB3tUXuULA=
github.com/coredns/coredns v1.3.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0=
github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
github.com/coredns/coredns v1.10.1 h1:6OyL7tcvYxeNHONj5iQlVM2GXBzAOq57L3/LUKP1DbA=
github.com/coredns/coredns v1.10.1/go.mod h1:oGgoY6cRrdJzKgNrsT30Hztu7/MutSHCYwqGDWngXCc=
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
@@ -346,12 +352,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11 h1:m8nX8hsUghn853BJ5qB0lX+VvS6LTJPksWyILFZRYN4=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11/go.mod h1:s1PfVYYVmTMgCSPtho4LKBDecEHJWtiVDPNv78Z985U=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/dnstap/golang-dnstap v0.4.0 h1:KRHBoURygdGtBjDI2w4HifJfMAhhOqDuktAokaSa234=
github.com/dnstap/golang-dnstap v0.4.0/go.mod h1:FqsSdH58NAmkAvKcpyxht7i4FoBjKu8E4JUPt8ipSUs=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@@ -370,11 +378,9 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
@@ -388,7 +394,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
@@ -396,8 +401,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZ
github.com/euank/go-kmsg-parser v2.0.0+incompatible h1:cHD53+PLQuuQyLZeriD1V/esuG4MuU0Pjs5y6iknohY=
github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710 h1:QdyRyGZWLEvJG5Kw3VcVJvhXJ5tZ1MkRgqpJOEZSySM=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI=
github.com/farsightsec/golang-framestream v0.3.0 h1:/spFQHucTle/ZIPkYqrfshQqPe2VQEzesH243TjIwqA=
github.com/farsightsec/golang-framestream v0.3.0/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -405,6 +410,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fernet/fernet-go v0.0.0-20180830025343-9eac43b88a5e h1:P10tZmVD2XclAaT9l7OduMH1OLFzTa1wUuUqHZnEdI0=
github.com/fernet/fernet-go v0.0.0-20180830025343-9eac43b88a5e/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c=
github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
@@ -449,7 +455,6 @@ github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568/go.mod h1:/
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
@@ -504,6 +509,9 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-plus/errors v1.0.0 h1:9Rh4itnph6OYO7wm9u3FUTLMFMKCoL07+bLiYKELxWw=
github.com/golang-plus/errors v1.0.0/go.mod h1:YTFZjOTBcUFieeZe4q+7Umfu/sceSr3XQyBIkTkzpd8=
github.com/golang-plus/testing v1.0.0 h1:azmFWFV44LHK/j/jAkNTbzB2G2b6HdmqF358r73m8kg=
@@ -511,6 +519,8 @@ github.com/golang-plus/testing v1.0.0/go.mod h1:psANDlKPZ0ycedUzCS0Trf8h98sFyOyB
github.com/golang-plus/uuid v1.0.0 h1:ga84hG89vda++AGpup4N/cbfbM3O4d3R5ON9/jcN6AY=
github.com/golang-plus/uuid v1.0.0/go.mod h1:pBDDRrdgRHHqyYlj1d1i2gwyBq62Zc+sP2cDiCDkBLI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -591,6 +601,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -623,7 +634,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282 h1:KFqmdzEPbU7Uck2tn50t+HQXZNVkxe8M9qRb/ZoSHaE=
github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
@@ -648,12 +658,17 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/huaweicloud/huaweicloud-sdk-go v1.0.26 h1:aWTl4Ng9lZUW1DupYyqYiqkTwuTKMmqUZBKLmX0iWxo=
github.com/huaweicloud/huaweicloud-sdk-go v1.0.26/go.mod h1:YHXxw/bm7AohI4jTY8Z43JYw+DPCDLqjxoLTqmcyja4=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inetaf/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:E9qjt3p/44HP4XfAABZhSgNZHUufBVKIm/nyPUlVpvY=
github.com/inetaf/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
github.com/influxdata/influxql v1.1.0 h1:sPsaumLFRPMwR5QtD3Up54HXpNND8Eu7G1vQFmi3quQ=
github.com/influxdata/influxql v1.1.0/go.mod h1:KpVI7okXjK6PRi3Z5B+mtKZli+R1DnZgb3N+tzevNgo=
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 h1:w66aaP3c6SIQ0pi3QH1Tb4AMO3aWoEPxd1CNvLphbkA=
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jaypipes/ghw v0.11.0 h1:i0pKvAM7eZk0KvLm9vzpcpDKTRnfR6AQ5pFkPVnYJXU=
@@ -756,8 +771,9 @@ github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9a
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -794,8 +810,9 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/caddy v0.10.11 h1:s8X+R8DuBbrrMuUTcWSxlDe567B0s5EDmiDBKSYsioY=
github.com/mholt/caddy v0.10.11/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989 h1:PS1dLCGtD8bb9RPKJrc8bS7qHL6JnW1CZvwzH9dPoUs=
github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY=
github.com/minio/cli v1.20.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY=
@@ -867,9 +884,11 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
@@ -878,6 +897,7 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -896,21 +916,25 @@ github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaL
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0 h1:uhcF5Jd7rP9DVEL10Siffyepr6SvlKbUsjH5JpNCRi8=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0/go.mod h1:+oCZ5GXXr7KPI/DNOQORPTq5AWHfALJj9c72b0+YsEY=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go-opentracing v0.3.4 h1:x/pBv/5VJNWkcHF1G9xqhug8Iw7X1y1zOMzDmyuvP2g=
github.com/openzipkin/zipkin-go-opentracing v0.3.4/go.mod h1:js2AbwmHW0YD9DwIw2JhQWmbfFi/UnWyYwdVhqbCDOE=
github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A=
github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM=
github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU=
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
@@ -995,8 +1019,9 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -1010,8 +1035,6 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -1040,6 +1063,9 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 h1:RpforrEYXWkmGwJHIGnLZ3tTWStkjVVstwzNGqxX2Ds=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/secure-systems-lab/go-securesystemslib v0.3.1/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U=
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@@ -1056,6 +1082,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -1102,6 +1129,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
@@ -1116,6 +1144,8 @@ github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EU
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
@@ -1177,12 +1207,12 @@ github.com/zexi/promql/v2 v2.12.1 h1:crHKpULdLLsBZ9b78Rg6qQkugzlk6BHeCj93tw/F5RU
github.com/zexi/promql/v2 v2.12.1/go.mod h1:2UtzWZGmth95n2qdIZWpJ5yQ0cE5hEvz0dGsajI1Sqg=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.etcd.io/etcd/api/v3 v3.5.7 h1:sbcmosSVesNrWOJ58ZQFitHMdncusIifYcrBfwrlJSY=
go.etcd.io/etcd/api/v3 v3.5.7/go.mod h1:9qew1gCdDDLu+VwmeG+iFpL+QlpHTo7iubavdVDgCAA=
go.etcd.io/etcd/client/pkg/v3 v3.5.7 h1:y3kf5Gbp4e4q7egZdn5T7W9TSHUvkClN6u+Rq9mEOmg=
go.etcd.io/etcd/client/pkg/v3 v3.5.7/go.mod h1:o0Abi1MK86iad3YrWhgUsbGx1pmTS+hrORWc2CamuhY=
go.etcd.io/etcd/client/v3 v3.5.7 h1:u/OhpiuCgYY8awOHlhIhmGIGpxfBU/GZBUP3m/3/Iz4=
go.etcd.io/etcd/client/v3 v3.5.7/go.mod h1:sOWmj9DZUMyAngS7QQwCyAXXAL6WhgTOPLNS/NabQgw=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -1213,6 +1243,11 @@ go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -1220,15 +1255,16 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
@@ -1261,7 +1297,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -1276,6 +1311,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1310,6 +1347,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -1321,6 +1359,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -1384,7 +1423,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1415,6 +1453,7 @@ golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1430,6 +1469,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1464,7 +1504,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
@@ -1497,7 +1536,6 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1505,6 +1543,7 @@ golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -1526,14 +1565,18 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1600,7 +1643,6 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 h1:SzXBGiWM1LNVYLCRP3e0/Gsze804l4jGoJ5lYysEO5I=
@@ -1624,7 +1666,6 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
@@ -1644,6 +1685,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/DataDog/dd-trace-go.v1 v1.47.0 h1:w3mHEgOR1o52mkyCbkTM+El8DG732+Fnug4FAGhIpsk=
gopkg.in/DataDog/dd-trace-go.v1 v1.47.0/go.mod h1:aHb6c4hPRANXnB64LDAKyfWotKgfRjlHv23MnahM8AI=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=

View File

@@ -20,17 +20,16 @@ import (
"strconv"
"strings"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/pkg/dnsutil"
"github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
_ "yunion.io/x/sqlchemy/backends"
@@ -73,7 +72,7 @@ type SRegionDNS struct {
Fall fall.F
Zones []string
PrimaryZone string
Upstream upstream.Upstream
Upstream *upstream.Upstream
SqlConnection string
AuthUrl string
AdminProject string
@@ -135,57 +134,65 @@ func (r *SRegionDNS) initAuth() {
}
func (r *SRegionDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, rmsg *dns.Msg) (int, error) {
log.Debugf("ServeDNS: %s", jsonutils.Marshal(rmsg).String())
var (
records []dns.RR
extra []dns.RR
err error
)
state := request.Request{W: w, Req: rmsg}
qname := state.Name()
zone := plugin.Zones(r.Zones).Matches(qname)
if zone == "" {
return plugin.NextOrFailure(r.Name(), r.Next, ctx, w, rmsg)
}
state.Zone = zone
opt := plugin.Options{}
state := request.Request{W: w, Req: rmsg, Context: ctx}
zone := plugin.Zones(r.Zones).Matches(state.Name())
switch state.QType() {
case dns.TypeA:
records, err = plugin.A(r, zone, state, nil, opt)
records, _, err = plugin.A(ctx, r, zone, state, nil, opt)
case dns.TypeAAAA:
records, err = plugin.AAAA(r, zone, state, nil, opt)
records, _, err = plugin.AAAA(ctx, r, zone, state, nil, opt)
case dns.TypeTXT:
records, err = plugin.TXT(r, zone, state, opt)
records, _, err = plugin.TXT(ctx, r, zone, state, nil, opt)
case dns.TypeCNAME:
records, err = plugin.CNAME(r, zone, state, opt)
records, err = plugin.CNAME(ctx, r, zone, state, opt)
case dns.TypePTR:
records, err = plugin.PTR(r, zone, state, opt)
records, err = plugin.PTR(ctx, r, zone, state, opt)
case dns.TypeMX:
records, extra, err = plugin.MX(r, zone, state, opt)
records, extra, err = plugin.MX(ctx, r, zone, state, opt)
case dns.TypeSRV:
records, extra, err = plugin.SRV(r, zone, state, opt)
records, extra, err = plugin.SRV(ctx, r, zone, state, opt)
case dns.TypeSOA:
records, err = plugin.SOA(r, zone, state, opt)
records, err = plugin.SOA(ctx, r, zone, state, opt)
case dns.TypeNS:
if state.Name() == zone {
records, extra, err = plugin.NS(r, zone, state, opt)
records, extra, err = plugin.NS(ctx, r, zone, state, opt)
break
}
fallthrough
default:
log.Warningf("Not processed state: %#v", state)
// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
_, err = plugin.A(r, zone, state, nil, opt)
_, _, err = plugin.A(ctx, r, zone, state, nil, opt)
}
if err == errCallNext {
if r.Fall.Through(state.Name()) {
return plugin.NextOrFailure(r.Name(), r.Next, ctx, w, rmsg)
}
return plugin.BackendError(r, zone, dns.RcodeNameError, state, nil /* err */, opt)
return plugin.BackendError(ctx, r, zone, dns.RcodeNameError, state, nil /* err */, opt)
} else if err == errRefused {
return plugin.BackendError(r, zone, dns.RcodeRefused, state, err, opt)
return plugin.BackendError(ctx, r, zone, dns.RcodeRefused, state, err, opt)
} else if err == errNotFound {
return plugin.BackendError(r, zone, dns.RcodeNameError, state, err, opt)
return plugin.BackendError(ctx, r, zone, dns.RcodeNameError, state, err, opt)
}
if len(records) == 0 {
return plugin.BackendError(r, zone, dns.RcodeNameError, state, err, opt)
return plugin.BackendError(ctx, r, zone, dns.RcodeNameError, state, err, opt)
}
m := new(dns.Msg)
@@ -207,7 +214,7 @@ var (
)
// Services implements the ServiceBackend interface
func (r *SRegionDNS) Services(state request.Request, exact bool, opt plugin.Options) ([]msg.Service, error) {
func (r *SRegionDNS) Services(ctx context.Context, state request.Request, exact bool, opt plugin.Options) ([]msg.Service, error) {
var services []msg.Service
var err error
@@ -255,7 +262,7 @@ func (r *SRegionDNS) Services(state request.Request, exact bool, opt plugin.Opti
return nil, errRefused
}
services, err = r.Records(state, false)
services, err = r.Records(ctx, state, false)
if err != nil {
// log.Errorf("Records %s fail: %s", state.Name(), err)
return nil, err
@@ -264,8 +271,8 @@ func (r *SRegionDNS) Services(state request.Request, exact bool, opt plugin.Opti
}
// Lookup implements the ServiceBackend interface
func (r *SRegionDNS) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
return r.Upstream.Lookup(state, name, typ)
func (r *SRegionDNS) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) {
return r.Upstream.Lookup(ctx, state, name, typ)
}
// IsNameError implements the ServiceBackend interface
@@ -274,7 +281,7 @@ func (r *SRegionDNS) IsNameError(err error) bool {
}
// Records looks up records in region mysql
func (r *SRegionDNS) Records(state request.Request, exact bool) ([]msg.Service, error) {
func (r *SRegionDNS) Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) {
req := parseRequest(state)
if r.InCloudOnly && !req.srcInCloud {

View File

@@ -15,6 +15,7 @@
package dns
import (
"context"
"strings"
"github.com/coredns/coredns/plugin"
@@ -26,10 +27,10 @@ import (
)
// Reverse implements the ServiceBackend interface
func (r *SRegionDNS) Reverse(state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
func (r *SRegionDNS) Reverse(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
ip := dnsutil.ExtractAddressFromReverse(state.Name())
if ip == "" {
_, e := r.Records(state, exact)
_, e := r.Records(ctx, state, exact)
return nil, e
}
records, err := r.getNameForIp(ip, state)

View File

@@ -17,13 +17,14 @@ package dns
import (
"context"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/fall"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/regutils"
@@ -31,67 +32,20 @@ import (
"yunion.io/x/onecloud/pkg/mcclient/modules/identity"
)
var log = clog.NewWithPlugin(PluginName)
func init() {
caddy.RegisterPlugin(PluginName, caddy.Plugin{
ServerType: "dns",
Action: setup,
})
plugin.Register(PluginName, setup)
}
func setup(c *caddy.Controller) error {
rDNS, err := regionDNSParse(c)
if err != nil {
return plugin.Error(PluginName, err)
}
rDNS.initAuth()
conf, err := identity.ServicesV3.GetConfig(rDNS.getAdminSession(context.Background()), apis.SERVICE_TYPE_REGION)
if err != nil {
return errors.Wrap(err, "GetConfig")
}
if conf.Contains("dns_domain") {
rDNS.PrimaryZone, _ = conf.GetString("dns_domain")
}
log.Infof("use dns_domain %q", rDNS.PrimaryZone)
if len(rDNS.PrimaryZone) > 0 && !regutils.MatchDomainName(rDNS.PrimaryZone) {
return errors.Wrapf(errors.ErrInvalidFormat, "dns_domain %q invalid", rDNS.PrimaryZone)
}
if len(rDNS.PrimaryZone) > 0 {
if r := rDNS.PrimaryZone[len(rDNS.PrimaryZone)-1]; r != '.' {
rDNS.PrimaryZone += "."
}
} else {
rDNS.PrimaryZone = ""
}
rDNS.primaryZoneLabelCount = dns.CountLabel(rDNS.PrimaryZone)
err = rDNS.initDB(c)
if err != nil {
return plugin.Error(PluginName, err)
}
/*if !rDNS.K8sSkip {
go rDNS.initK8s()
}*/
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
rDNS.Next = next
return rDNS
})
return nil
}
func regionDNSParse(c *caddy.Controller) (*SRegionDNS, error) {
return parseConfig(c)
}
func parseConfig(c *caddy.Controller) (*SRegionDNS, error) {
rDNS := New()
for c.Next() {
log.Infof("setup: %s", PluginName)
var fallF fall.F
upStream := upstream.New()
rDNS := New()
rDNS.Zones = c.RemainingArgs()
if len(rDNS.Zones) == 0 {
rDNS.Zones = make([]string, len(c.ServerBlockKeys))
@@ -101,78 +55,107 @@ func parseConfig(c *caddy.Controller) (*SRegionDNS, error) {
rDNS.Zones[i] = plugin.Host(str).Normalize()
}
if c.NextBlock() {
for {
switch c.Val() {
case "dns_domain":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.PrimaryZone = c.Val()
case "fallthrough":
rDNS.Fall.SetZonesFromArgs(c.RemainingArgs())
case "sql_connection":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.SqlConnection = c.Val()
case "auth_url":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AuthUrl = c.Val()
case "admin_project":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AdminProject = c.Val()
case "admin_project_domain":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AdminProjectDomain = c.Val()
case "admin_user":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AdminUser = c.Val()
case "admin_domain":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AdminDomain = c.Val()
case "admin_password":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.AdminPassword = c.Val()
case "region":
if !c.NextArg() {
return nil, c.ArgErr()
}
rDNS.Region = c.Val()
case "upstream":
args := c.RemainingArgs()
u, err := upstream.New(args)
if err != nil {
return nil, err
}
rDNS.Upstream = u
case "in_cloud_only":
rDNS.InCloudOnly = true
// case "k8s_skip":
// rDNS.K8sSkip = true
default:
if c.Val() != "}" {
return nil, c.Errf("unknown property %q", c.Val())
}
for c.NextBlock() {
switch c.Val() {
case "dns_domain":
if !c.NextArg() {
return plugin.Error(PluginName+".dns_domain", c.ArgErr())
}
if !c.Next() {
break
rDNS.PrimaryZone = c.Val()
case "sql_connection":
if !c.NextArg() {
return plugin.Error(PluginName+".sql_connection", c.ArgErr())
}
rDNS.SqlConnection = c.Val()
case "auth_url":
if !c.NextArg() {
return plugin.Error(PluginName+".auth_url", c.ArgErr())
}
rDNS.AuthUrl = c.Val()
case "admin_project":
if !c.NextArg() {
return plugin.Error(PluginName+".admin_project", c.ArgErr())
}
rDNS.AdminProject = c.Val()
case "admin_project_domain":
if !c.NextArg() {
return plugin.Error(PluginName+".admin_project_domain", c.ArgErr())
}
rDNS.AdminProjectDomain = c.Val()
case "admin_user":
if !c.NextArg() {
return plugin.Error(PluginName+".admin_user", c.ArgErr())
}
rDNS.AdminUser = c.Val()
case "admin_domain":
if !c.NextArg() {
return plugin.Error(PluginName+".admin_domain", c.ArgErr())
}
rDNS.AdminDomain = c.Val()
case "admin_password":
if !c.NextArg() {
return plugin.Error(PluginName+".admin_password", c.ArgErr())
}
rDNS.AdminPassword = c.Val()
case "region":
if !c.NextArg() {
return plugin.Error(PluginName+".region", c.ArgErr())
}
rDNS.Region = c.Val()
case "in_cloud_only":
rDNS.InCloudOnly = true
case "upstream":
c.RemainingArgs()
case "fallthrough":
fallF.SetZonesFromArgs(c.RemainingArgs())
default:
return plugin.Error(PluginName, c.Errf("unknown property %q", c.Val()))
}
}
ctx, cancel := context.WithCancel(context.Background())
rDNS.initAuth()
conf, err := identity.ServicesV3.GetConfig(rDNS.getAdminSession(ctx), apis.SERVICE_TYPE_REGION)
if err != nil {
cancel()
return errors.Wrap(err, "GetConfig")
}
if conf.Contains("dns_domain") {
rDNS.PrimaryZone, _ = conf.GetString("dns_domain")
}
log.Infof("use dns_domain %q", rDNS.PrimaryZone)
if len(rDNS.PrimaryZone) > 0 && !regutils.MatchDomainName(rDNS.PrimaryZone) {
cancel()
return errors.Wrapf(errors.ErrInvalidFormat, "dns_domain %q invalid", rDNS.PrimaryZone)
}
if len(rDNS.PrimaryZone) > 0 {
if r := rDNS.PrimaryZone[len(rDNS.PrimaryZone)-1]; r != '.' {
rDNS.PrimaryZone += "."
}
} else {
rDNS.PrimaryZone = ""
}
rDNS.primaryZoneLabelCount = dns.CountLabel(rDNS.PrimaryZone)
err = rDNS.initDB(c)
if err != nil {
cancel()
return plugin.Error(PluginName, err)
}
rDNS.Fall = fallF
rDNS.Upstream = upStream
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
rDNS.Next = next
return rDNS
})
c.OnShutdown(func() error { cancel(); return nil })
}
return rDNS, nil
return nil
}

View File

@@ -1,13 +1,15 @@
# The standard Go .gitignore file follows. (Sourced from: github.com/github/gitignore/master/Go.gitignore)
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
*.test
# Folders
_obj
_test
.vagrant
.DS_Store
.idea/
.vscode/
# Architecture specific extensions/prefixes
*.[568vq]
@@ -22,6 +24,9 @@ _cgo_export.*
_testmain.go
*.exe
*.test
*.prof
coverage.txt
profile.out
# go-autorest specific
vendor/
autorest/azure/example/example

1004
vendor/github.com/Azure/go-autorest/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

23
vendor/github.com/Azure/go-autorest/GNUmakefile generated vendored Normal file
View File

@@ -0,0 +1,23 @@
DIR?=./autorest/
default: build
build: fmt
go install $(DIR)
test:
go test $(DIR) || exit 1
vet:
@echo "go vet ."
@go vet $(DIR)... ; if [ $$? -eq 1 ]; then \
echo ""; \
echo "Vet found suspicious constructs. Please check the reported constructs"; \
echo "and fix them if necessary before submitting the code for review."; \
exit 1; \
fi
fmt:
gofmt -w $(DIR)
.PHONY: build test vet fmt

324
vendor/github.com/Azure/go-autorest/Gopkg.lock generated vendored Normal file
View File

@@ -0,0 +1,324 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:892e39e5c083d0943f1e80ab8351690f183c6a5ab24e1d280adcad424c26255e"
name = "contrib.go.opencensus.io/exporter/ocagent"
packages = ["."]
pruneopts = "UT"
revision = "a8a6f458bbc1d5042322ad1f9b65eeb0b69be9ea"
version = "v0.6.0"
[[projects]]
digest = "1:8f5acd4d4462b5136af644d25101f0968a7a94ee90fcb2059cec5b7cc42e0b20"
name = "github.com/census-instrumentation/opencensus-proto"
packages = [
"gen-go/agent/common/v1",
"gen-go/agent/metrics/v1",
"gen-go/agent/trace/v1",
"gen-go/metrics/v1",
"gen-go/resource/v1",
"gen-go/trace/v1",
]
pruneopts = "UT"
revision = "d89fa54de508111353cb0b06403c00569be780d8"
version = "v0.2.1"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
digest = "1:cf0d2e435fd4ce45b789e93ef24b5f08e86be0e9807a16beb3694e2d8c9af965"
name = "github.com/dimchansky/utfbom"
packages = ["."]
pruneopts = "UT"
revision = "d2133a1ce379ef6fa992b0514a77146c60db9d1c"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:b7cb6054d3dff43b38ad2e92492f220f57ae6087ee797dca298139776749ace8"
name = "github.com/golang/groupcache"
packages = ["lru"]
pruneopts = "UT"
revision = "611e8accdfc92c4187d399e95ce826046d4c8d73"
[[projects]]
digest = "1:e3839df32927e8d3403cd5aa7253d966e8ff80fc8f10e2e35d146461cd83fcfa"
name = "github.com/golang/protobuf"
packages = [
"descriptor",
"jsonpb",
"proto",
"protoc-gen-go/descriptor",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/struct",
"ptypes/timestamp",
"ptypes/wrappers",
]
pruneopts = "UT"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
digest = "1:c560cd79300fac84f124b96225181a637a70b60155919a3c36db50b7cca6b806"
name = "github.com/grpc-ecosystem/grpc-gateway"
packages = [
"internal",
"runtime",
"utilities",
]
pruneopts = "UT"
revision = "f7120437bb4f6c71f7f5076ad65a45310de2c009"
version = "v1.12.1"
[[projects]]
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
version = "v1.1.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:99d32780e5238c2621fff621123997c3e3cca96db8be13179013aea77dfab551"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = "UT"
revision = "221dbe5ed46703ee255b1da0dec05086f5035f62"
version = "v1.4.0"
[[projects]]
digest = "1:7c5e00383399fe13de0b4b65c9fdde16275407ce8ac02d867eafeaa916edcc71"
name = "go.opencensus.io"
packages = [
".",
"internal",
"internal/tagencoding",
"metric/metricdata",
"metric/metricproducer",
"plugin/ocgrpc",
"plugin/ochttp",
"plugin/ochttp/propagation/b3",
"plugin/ochttp/propagation/tracecontext",
"resource",
"stats",
"stats/internal",
"stats/view",
"tag",
"trace",
"trace/internal",
"trace/propagation",
"trace/tracestate",
]
pruneopts = "UT"
revision = "aad2c527c5defcf89b5afab7f37274304195a6b2"
version = "v0.22.2"
[[projects]]
branch = "master"
digest = "1:f604f5e2ee721b6757d962dfe7bab4f28aae50c456e39cfb2f3819762a44a6ae"
name = "golang.org/x/crypto"
packages = [
"pkcs12",
"pkcs12/internal/rc2",
]
pruneopts = "UT"
revision = "e9b2fee46413994441b28dfca259d911d963dfed"
[[projects]]
branch = "master"
digest = "1:334b27eac455cb6567ea28cd424230b07b1a64334a2f861a8075ac26ce10af43"
name = "golang.org/x/lint"
packages = [
".",
"golint",
]
pruneopts = "UT"
revision = "fdd1cda4f05fd1fd86124f0ef9ce31a0b72c8448"
[[projects]]
branch = "master"
digest = "1:257a75d024975428ab9192bfc334c3490882f8cb21322ea5784ca8eca000a910"
name = "golang.org/x/net"
packages = [
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace",
]
pruneopts = "UT"
revision = "1ddd1de85cb0337b623b740a609d35817d516a8d"
[[projects]]
branch = "master"
digest = "1:382bb5a7fb4034db3b6a2d19e5a4a6bcf52f4750530603c01ca18a172fa3089b"
name = "golang.org/x/sync"
packages = ["semaphore"]
pruneopts = "UT"
revision = "cd5d95a43a6e21273425c7ae415d3df9ea832eeb"
[[projects]]
branch = "master"
digest = "1:4da420ceda5f68e8d748aa2169d0ed44ffadb1bbd6537cf778a49563104189b8"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945"
[[projects]]
digest = "1:8d8faad6b12a3a4c819a3f9618cb6ee1fa1cfc33253abeeea8b55336721e3405"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/language",
"internal/language/compact",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[[projects]]
branch = "master"
digest = "1:4eb5ea8395fb60212dd58b92c9db80bab59d5e99c7435f9a6a0a528c373b60e7"
name = "golang.org/x/tools"
packages = [
"go/ast/astutil",
"go/gcexportdata",
"go/internal/gcimporter",
"go/types/typeutil",
]
pruneopts = "UT"
revision = "259af5ff87bdcd4abf2ecda8edc3f13f04f26a42"
[[projects]]
digest = "1:964bb30febc27fabfbec4759fa530c6ec35e77a7c85fed90b9317ea39a054877"
name = "google.golang.org/api"
packages = ["support/bundler"]
pruneopts = "UT"
revision = "8a410c21381766a810817fd6200fce8838ecb277"
version = "v0.14.0"
[[projects]]
branch = "master"
digest = "1:a8d5c2c6e746b3485e36908ab2a9e3d77b86b81f8156d88403c7d2b462431dfd"
name = "google.golang.org/genproto"
packages = [
"googleapis/api/httpbody",
"googleapis/rpc/status",
"protobuf/field_mask",
]
pruneopts = "UT"
revision = "51378566eb590fa106d1025ea12835a4416dda84"
[[projects]]
digest = "1:b59ce3ddb11daeeccccc9cb3183b58ebf8e9a779f1c853308cd91612e817a301"
name = "google.golang.org/grpc"
packages = [
".",
"backoff",
"balancer",
"balancer/base",
"balancer/roundrobin",
"binarylog/grpc_binarylog_v1",
"codes",
"connectivity",
"credentials",
"credentials/internal",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/balancerload",
"internal/binarylog",
"internal/buffer",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/grpcsync",
"internal/resolver/dns",
"internal/resolver/passthrough",
"internal/syscall",
"internal/transport",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"serviceconfig",
"stats",
"status",
"tap",
]
pruneopts = "UT"
revision = "1a3960e4bd028ac0cec0a2afd27d7d8e67c11514"
version = "v1.25.1"
[[projects]]
digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "1f64d6156d11335c3f22d9330b0ad14fc1e789ce"
version = "v2.2.7"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"contrib.go.opencensus.io/exporter/ocagent",
"github.com/dgrijalva/jwt-go",
"github.com/dimchansky/utfbom",
"github.com/mitchellh/go-homedir",
"github.com/stretchr/testify/require",
"go.opencensus.io/plugin/ochttp",
"go.opencensus.io/plugin/ochttp/propagation/tracecontext",
"go.opencensus.io/stats/view",
"go.opencensus.io/trace",
"golang.org/x/crypto/pkcs12",
"golang.org/x/lint/golint",
]
solver-name = "gps-cdcl"
solver-version = 1

59
vendor/github.com/Azure/go-autorest/Gopkg.toml generated vendored Normal file
View File

@@ -0,0 +1,59 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
required = ["golang.org/x/lint/golint"]
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "contrib.go.opencensus.io/exporter/ocagent"
version = "0.6.0"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
[[constraint]]
name = "github.com/dimchansky/utfbom"
version = "1.1.0"
[[constraint]]
name = "github.com/mitchellh/go-homedir"
version = "1.1.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.3.0"
[[constraint]]
name = "go.opencensus.io"
version = "0.22.0"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"

191
vendor/github.com/Azure/go-autorest/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2015 Microsoft Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

165
vendor/github.com/Azure/go-autorest/README.md generated vendored Normal file
View File

@@ -0,0 +1,165 @@
# go-autorest
[![GoDoc](https://godoc.org/github.com/Azure/go-autorest/autorest?status.png)](https://godoc.org/github.com/Azure/go-autorest/autorest)
[![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/go/Azure.go-autorest?branchName=master)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=625&branchName=master)
[![Go Report Card](https://goreportcard.com/badge/Azure/go-autorest)](https://goreportcard.com/report/Azure/go-autorest)
Package go-autorest provides an HTTP request client for use with [Autorest](https://github.com/Azure/autorest.go)-generated API client packages.
An authentication client tested with Azure Active Directory (AAD) is also
provided in this repo in the package
`github.com/Azure/go-autorest/autorest/adal`. Despite its name, this package
is maintained only as part of the Azure Go SDK and is not related to other
"ADAL" libraries in [github.com/AzureAD](https://github.com/AzureAD).
## Overview
Package go-autorest implements an HTTP request pipeline suitable for use across
multiple goroutines and provides the shared routines used by packages generated
by [Autorest](https://github.com/Azure/autorest.go).
The package breaks sending and responding to HTTP requests into three phases: Preparing, Sending,
and Responding. A typical pattern is:
```go
req, err := Prepare(&http.Request{},
token.WithAuthorization())
resp, err := Send(req,
WithLogging(logger),
DoErrorIfStatusCode(http.StatusInternalServerError),
DoCloseIfError(),
DoRetryForAttempts(5, time.Second))
err = Respond(resp,
ByDiscardingBody(),
ByClosing())
```
Each phase relies on decorators to modify and / or manage processing. Decorators may first modify
and then pass the data along, pass the data first and then modify the result, or wrap themselves
around passing the data (such as a logger might do). Decorators run in the order provided. For
example, the following:
```go
req, err := Prepare(&http.Request{},
WithBaseURL("https://microsoft.com/"),
WithPath("a"),
WithPath("b"),
WithPath("c"))
```
will set the URL to:
```
https://microsoft.com/a/b/c
```
Preparers and Responders may be shared and re-used (assuming the underlying decorators support
sharing and re-use). Performant use is obtained by creating one or more Preparers and Responders
shared among multiple go-routines, and a single Sender shared among multiple sending go-routines,
all bound together by means of input / output channels.
Decorators hold their passed state within a closure (such as the path components in the example
above). Be careful to share Preparers and Responders only in a context where such held state
applies. For example, it may not make sense to share a Preparer that applies a query string from a
fixed set of values. Similarly, sharing a Responder that reads the response body into a passed
struct (e.g., `ByUnmarshallingJson`) is likely incorrect.
Errors raised by autorest objects and methods will conform to the `autorest.Error` interface.
See the included examples for more detail. For details on the suggested use of this package by
generated clients, see the Client described below.
## Helpers
### Handling Swagger Dates
The Swagger specification (https://swagger.io) that drives AutoRest
(https://github.com/Azure/autorest/) precisely defines two date forms: date and date-time. The
github.com/Azure/go-autorest/autorest/date package provides time.Time derivations to ensure correct
parsing and formatting.
### Handling Empty Values
In JSON, missing values have different semantics than empty values. This is especially true for
services using the HTTP PATCH verb. The JSON submitted with a PATCH request generally contains
only those values to modify. Missing values are to be left unchanged. Developers, then, require a
means to both specify an empty value and to leave the value out of the submitted JSON.
The Go JSON package (`encoding/json`) supports the `omitempty` tag. When specified, it omits
empty values from the rendered JSON. Since Go defines default values for all base types (such as ""
for string and 0 for int) and provides no means to mark a value as actually empty, the JSON package
treats default values as meaning empty, omitting them from the rendered JSON. This means that, using
the Go base types encoded through the default JSON package, it is not possible to create JSON to
clear a value at the server.
The workaround within the Go community is to use pointers to base types in lieu of base types within
structures that map to JSON. For example, instead of a value of type `string`, the workaround uses
`*string`. While this enables distinguishing empty values from those to be unchanged, creating
pointers to a base type (notably constant, in-line values) requires additional variables. This, for
example,
```go
s := struct {
S *string
}{ S: &"foo" }
```
fails, while, this
```go
v := "foo"
s := struct {
S *string
}{ S: &v }
```
succeeds.
To ease using pointers, the subpackage `to` contains helpers that convert to and from pointers for
Go base types which have Swagger analogs. It also provides a helper that converts between
`map[string]string` and `map[string]*string`, enabling the JSON to specify that the value
associated with a key should be cleared. With the helpers, the previous example becomes
```go
s := struct {
S *string
}{ S: to.StringPtr("foo") }
```
## Install
```bash
go get github.com/Azure/go-autorest/autorest
go get github.com/Azure/go-autorest/autorest/azure
go get github.com/Azure/go-autorest/autorest/date
go get github.com/Azure/go-autorest/autorest/to
```
### Using with Go Modules
In [v12.0.1](https://github.com/Azure/go-autorest/pull/386), this repository introduced the following modules.
- autorest/adal
- autorest/azure/auth
- autorest/azure/cli
- autorest/date
- autorest/mocks
- autorest/to
- autorest/validation
- autorest
- logger
- tracing
Tagging cumulative SDK releases as a whole (e.g. `v12.3.0`) is still enabled to support consumers of this repo that have not yet migrated to modules.
## License
See LICENSE file.
-----
This project has adopted the [Microsoft Open Source Code of
Conduct](https://opensource.microsoft.com/codeofconduct/). For more information
see the [Code of Conduct
FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact
[opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional
questions or comments.

View File

@@ -222,6 +222,10 @@ func CheckForUserCompletionWithContext(ctx context.Context, sender Sender, code
case "code_expired":
return nil, ErrDeviceCodeExpired
default:
// return a more meaningful error message if available
if token.ErrorDescription != nil {
return nil, fmt.Errorf("%s %s: %s", logPrefix, *token.Error, *token.ErrorDescription)
}
return nil, ErrDeviceGeneric
}
}

View File

@@ -1,3 +1,4 @@
//go:build modhack
// +build modhack
package adal
@@ -16,9 +17,9 @@ package adal
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest/autorest"
import _ "github.com/Azure/go-autorest"

View File

@@ -15,11 +15,24 @@ package adal
// limitations under the License.
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"golang.org/x/crypto/pkcs12"
)
var (
// ErrMissingCertificate is returned when no local certificate is found in the provided PFX data.
ErrMissingCertificate = errors.New("adal: certificate missing")
// ErrMissingPrivateKey is returned when no private key is found in the provided PFX data.
ErrMissingPrivateKey = errors.New("adal: private key missing")
)
// LoadToken restores a Token object from a file located at 'path'.
@@ -71,3 +84,52 @@ func SaveToken(path string, mode os.FileMode, token Token) error {
}
return nil
}
// DecodePfxCertificateData extracts the x509 certificate and RSA private key from the provided PFX data.
// The PFX data must contain a private key along with a certificate whose public key matches that of the
// private key or an error is returned.
// If the private key is not password protected pass the empty string for password.
func DecodePfxCertificateData(pfxData []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
blocks, err := pkcs12.ToPEM(pfxData, password)
if err != nil {
return nil, nil, err
}
// first extract the private key
var priv *rsa.PrivateKey
for _, block := range blocks {
if block.Type == "PRIVATE KEY" {
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, err
}
break
}
}
if priv == nil {
return nil, nil, ErrMissingPrivateKey
}
// now find the certificate with the matching public key of our private key
var cert *x509.Certificate
for _, block := range blocks {
if block.Type == "CERTIFICATE" {
pcert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
certKey, ok := pcert.PublicKey.(*rsa.PublicKey)
if !ok {
// keep looking
continue
}
if priv.E == certKey.E && priv.N.Cmp(certKey.N) == 0 {
// found a match
cert = pcert
break
}
}
}
if cert == nil {
return nil, nil, ErrMissingCertificate
}
return cert, priv, nil
}

View File

@@ -16,9 +16,11 @@ package adal
import (
"crypto/tls"
"net"
"net/http"
"net/http/cookiejar"
"sync"
"time"
"github.com/Azure/go-autorest/tracing"
)
@@ -28,6 +30,7 @@ const (
mimeTypeFormPost = "application/x-www-form-urlencoded"
)
// DO NOT ACCESS THIS DIRECTLY. go through sender()
var defaultSender Sender
var defaultSenderInit = &sync.Once{}
@@ -71,15 +74,18 @@ func sender() Sender {
// note that we can't init defaultSender in init() since it will
// execute before calling code has had a chance to enable tracing
defaultSenderInit.Do(func() {
// Use behaviour compatible with DefaultTransport, but require TLS minimum version.
defaultTransport := http.DefaultTransport.(*http.Transport)
// copied from http.DefaultTransport with a TLS minimum version.
transport := &http.Transport{
Proxy: defaultTransport.Proxy,
DialContext: defaultTransport.DialContext,
MaxIdleConns: defaultTransport.MaxIdleConns,
IdleConnTimeout: defaultTransport.IdleConnTimeout,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},

View File

@@ -30,12 +30,14 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/Azure/go-autorest/autorest/date"
"github.com/dgrijalva/jwt-go"
"github.com/Azure/go-autorest/logger"
"github.com/golang-jwt/jwt/v4"
)
const (
@@ -62,14 +64,29 @@ const (
// msiEndpoint is the well known endpoint for getting MSI authentications tokens
msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
// the API version to use for the MSI endpoint
msiAPIVersion = "2018-02-01"
// the default number of attempts to refresh an MSI authentication token
defaultMaxMSIRefreshAttempts = 5
// asMSIEndpointEnv is the environment variable used to store the endpoint on App Service and Functions
asMSIEndpointEnv = "MSI_ENDPOINT"
msiEndpointEnv = "MSI_ENDPOINT"
// asMSISecretEnv is the environment variable used to store the request secret on App Service and Functions
asMSISecretEnv = "MSI_SECRET"
msiSecretEnv = "MSI_SECRET"
// the API version to use for the legacy App Service MSI endpoint
appServiceAPIVersion2017 = "2017-09-01"
// secret header used when authenticating against app service MSI endpoint
secretHeader = "Secret"
// the format for expires_on in UTC with AM/PM
expiresOnDateFormatPM = "1/2/2006 15:04:05 PM +00:00"
// the format for expires_on in UTC without AM/PM
expiresOnDateFormat = "1/2/2006 15:04:05 +00:00"
)
// OAuthTokenProvider is an interface which should be implemented by an access token retriever
@@ -276,6 +293,8 @@ func (secret ServicePrincipalCertificateSecret) MarshalJSON() ([]byte, error) {
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
type ServicePrincipalMSISecret struct {
msiType msiType
clientResourceID string
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
@@ -354,6 +373,7 @@ type ServicePrincipalToken struct {
customRefreshFunc TokenRefresh
refreshCallbacks []TokenRefreshCallback
// MaxMSIRefreshAttempts is the maximum number of attempts to refresh an MSI token.
// Settings this to a value less than 1 will use the default value.
MaxMSIRefreshAttempts int
}
@@ -645,78 +665,167 @@ func NewServicePrincipalTokenFromAuthorizationCode(oauthConfig OAuthConfig, clie
)
}
type msiType int
const (
msiTypeUnavailable msiType = iota
msiTypeAppServiceV20170901
msiTypeCloudShell
msiTypeIMDS
)
func (m msiType) String() string {
switch m {
case msiTypeAppServiceV20170901:
return "AppServiceV20170901"
case msiTypeCloudShell:
return "CloudShell"
case msiTypeIMDS:
return "IMDS"
default:
return fmt.Sprintf("unhandled MSI type %d", m)
}
}
// returns the MSI type and endpoint, or an error
func getMSIType() (msiType, string, error) {
if endpointEnvVar := os.Getenv(msiEndpointEnv); endpointEnvVar != "" {
// if the env var MSI_ENDPOINT is set
if secretEnvVar := os.Getenv(msiSecretEnv); secretEnvVar != "" {
// if BOTH the env vars MSI_ENDPOINT and MSI_SECRET are set the msiType is AppService
return msiTypeAppServiceV20170901, endpointEnvVar, nil
}
// if ONLY the env var MSI_ENDPOINT is set the msiType is CloudShell
return msiTypeCloudShell, endpointEnvVar, nil
}
// if MSI_ENDPOINT is NOT set assume the msiType is IMDS
return msiTypeIMDS, msiEndpoint, nil
}
// GetMSIVMEndpoint gets the MSI endpoint on Virtual Machines.
// NOTE: this always returns the IMDS endpoint, it does not work for app services or cloud shell.
// Deprecated: NewServicePrincipalTokenFromMSI() and variants will automatically detect the endpoint.
func GetMSIVMEndpoint() (string, error) {
return msiEndpoint, nil
}
func isAppService() bool {
_, asMSIEndpointEnvExists := os.LookupEnv(asMSIEndpointEnv)
_, asMSISecretEnvExists := os.LookupEnv(asMSISecretEnv)
return asMSIEndpointEnvExists && asMSISecretEnvExists
}
// GetMSIAppServiceEndpoint get the MSI endpoint for App Service and Functions
// GetMSIAppServiceEndpoint get the MSI endpoint for App Service and Functions.
// It will return an error when not running in an app service/functions environment.
// Deprecated: NewServicePrincipalTokenFromMSI() and variants will automatically detect the endpoint.
func GetMSIAppServiceEndpoint() (string, error) {
asMSIEndpoint, asMSIEndpointEnvExists := os.LookupEnv(asMSIEndpointEnv)
if asMSIEndpointEnvExists {
return asMSIEndpoint, nil
msiType, endpoint, err := getMSIType()
if err != nil {
return "", err
}
switch msiType {
case msiTypeAppServiceV20170901:
return endpoint, nil
default:
return "", fmt.Errorf("%s is not app service environment", msiType)
}
return "", errors.New("MSI endpoint not found")
}
// GetMSIEndpoint get the appropriate MSI endpoint depending on the runtime environment
// Deprecated: NewServicePrincipalTokenFromMSI() and variants will automatically detect the endpoint.
func GetMSIEndpoint() (string, error) {
if isAppService() {
return GetMSIAppServiceEndpoint()
}
return GetMSIVMEndpoint()
_, endpoint, err := getMSIType()
return endpoint, err
}
// NewServicePrincipalTokenFromMSI creates a ServicePrincipalToken via the MSI VM Extension.
// It will use the system assigned identity when creating the token.
// msiEndpoint - empty string, or pass a non-empty string to override the default value.
// Deprecated: use NewServicePrincipalTokenFromManagedIdentity() instead.
func NewServicePrincipalTokenFromMSI(msiEndpoint, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, nil, callbacks...)
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, "", "", callbacks...)
}
// NewServicePrincipalTokenFromMSIWithUserAssignedID creates a ServicePrincipalToken via the MSI VM Extension.
// It will use the specified user assigned identity when creating the token.
// It will use the clientID of specified user assigned identity when creating the token.
// msiEndpoint - empty string, or pass a non-empty string to override the default value.
// Deprecated: use NewServicePrincipalTokenFromManagedIdentity() instead.
func NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, resource string, userAssignedID string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, &userAssignedID, callbacks...)
}
func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedID *string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if err := validateStringParam(msiEndpoint, "msiEndpoint"); err != nil {
if err := validateStringParam(userAssignedID, "userAssignedID"); err != nil {
return nil, err
}
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, userAssignedID, "", callbacks...)
}
// NewServicePrincipalTokenFromMSIWithIdentityResourceID creates a ServicePrincipalToken via the MSI VM Extension.
// It will use the azure resource id of user assigned identity when creating the token.
// msiEndpoint - empty string, or pass a non-empty string to override the default value.
// Deprecated: use NewServicePrincipalTokenFromManagedIdentity() instead.
func NewServicePrincipalTokenFromMSIWithIdentityResourceID(msiEndpoint, resource string, identityResourceID string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if err := validateStringParam(identityResourceID, "identityResourceID"); err != nil {
return nil, err
}
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, "", identityResourceID, callbacks...)
}
// ManagedIdentityOptions contains optional values for configuring managed identity authentication.
type ManagedIdentityOptions struct {
// ClientID is the user-assigned identity to use during authentication.
// It is mutually exclusive with IdentityResourceID.
ClientID string
// IdentityResourceID is the resource ID of the user-assigned identity to use during authentication.
// It is mutually exclusive with ClientID.
IdentityResourceID string
}
// NewServicePrincipalTokenFromManagedIdentity creates a ServicePrincipalToken using a managed identity.
// It supports the following managed identity environments.
// - App Service Environment (API version 2017-09-01 only)
// - Cloud shell
// - IMDS with a system or user assigned identity
func NewServicePrincipalTokenFromManagedIdentity(resource string, options *ManagedIdentityOptions, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if options == nil {
options = &ManagedIdentityOptions{}
}
return newServicePrincipalTokenFromMSI("", resource, options.ClientID, options.IdentityResourceID, callbacks...)
}
func newServicePrincipalTokenFromMSI(msiEndpoint, resource, userAssignedID, identityResourceID string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
if userAssignedID != nil {
if err := validateStringParam(*userAssignedID, "userAssignedID"); err != nil {
return nil, err
}
if userAssignedID != "" && identityResourceID != "" {
return nil, errors.New("cannot specify userAssignedID and identityResourceID")
}
// We set the oauth config token endpoint to be MSI's endpoint
msiEndpointURL, err := url.Parse(msiEndpoint)
msiType, endpoint, err := getMSIType()
if err != nil {
logger.Instance.Writef(logger.LogError, "Error determining managed identity environment: %v\n", err)
return nil, err
}
logger.Instance.Writef(logger.LogInfo, "Managed identity environment is %s, endpoint is %s\n", msiType, endpoint)
if msiEndpoint != "" {
endpoint = msiEndpoint
logger.Instance.Writef(logger.LogInfo, "Managed identity custom endpoint is %s\n", endpoint)
}
msiEndpointURL, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("resource", resource)
// App Service MSI currently only supports token API version 2017-09-01
if isAppService() {
v.Set("api-version", "2017-09-01")
} else {
v.Set("api-version", "2018-02-01")
// cloud shell sends its data in the request body
if msiType != msiTypeCloudShell {
v := url.Values{}
v.Set("resource", resource)
clientIDParam := "client_id"
switch msiType {
case msiTypeAppServiceV20170901:
clientIDParam = "clientid"
v.Set("api-version", appServiceAPIVersion2017)
break
case msiTypeIMDS:
v.Set("api-version", msiAPIVersion)
}
if userAssignedID != "" {
v.Set(clientIDParam, userAssignedID)
} else if identityResourceID != "" {
v.Set("mi_res_id", identityResourceID)
}
msiEndpointURL.RawQuery = v.Encode()
}
if userAssignedID != nil {
v.Set("client_id", *userAssignedID)
}
msiEndpointURL.RawQuery = v.Encode()
spt := &ServicePrincipalToken{
inner: servicePrincipalToken{
@@ -724,10 +833,14 @@ func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedI
OauthConfig: OAuthConfig{
TokenEndpoint: *msiEndpointURL,
},
Secret: &ServicePrincipalMSISecret{},
Secret: &ServicePrincipalMSISecret{
msiType: msiType,
clientResourceID: identityResourceID,
},
Resource: resource,
AutoRefresh: true,
RefreshWithin: defaultRefresh,
ClientID: userAssignedID,
},
refreshLock: &sync.RWMutex{},
sender: sender(),
@@ -735,10 +848,6 @@ func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedI
MaxMSIRefreshAttempts: defaultMaxMSIRefreshAttempts,
}
if userAssignedID != nil {
spt.inner.ClientID = *userAssignedID
}
return spt, nil
}
@@ -771,8 +880,9 @@ func (spt *ServicePrincipalToken) EnsureFresh() error {
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (spt *ServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if spt.inner.AutoRefresh && spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
// take the write lock then check to see if the token was already refreshed
// must take the read lock when initially checking the token's expiration
if spt.inner.AutoRefresh && spt.Token().WillExpireIn(spt.inner.RefreshWithin) {
// take the write lock then check again to see if the token was already refreshed
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
if spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
@@ -834,14 +944,6 @@ func (spt *ServicePrincipalToken) getGrantType() string {
}
}
func isIMDS(u url.URL) bool {
imds, err := url.Parse(msiEndpoint)
if err != nil {
return false
}
return (u.Host == imds.Host && u.Path == imds.Path) || isAppService()
}
func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource string) error {
if spt.customRefreshFunc != nil {
token, err := spt.customRefreshFunc(ctx, resource)
@@ -851,19 +953,45 @@ func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource
spt.inner.Token = *token
return spt.InvokeRefreshCallbacks(spt.inner.Token)
}
req, err := http.NewRequest(http.MethodPost, spt.inner.OauthConfig.TokenEndpoint.String(), nil)
if err != nil {
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
}
req.Header.Add("User-Agent", UserAgent())
// Add header when runtime is on App Service or Functions
if isAppService() {
asMSISecret, _ := os.LookupEnv(asMSISecretEnv)
req.Header.Add("Secret", asMSISecret)
}
req = req.WithContext(ctx)
if !isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
var resp *http.Response
authBodyFilter := func(b []byte) []byte {
if logger.Level() != logger.LogAuth {
return []byte("**REDACTED** authentication body")
}
return b
}
if msiSecret, ok := spt.inner.Secret.(*ServicePrincipalMSISecret); ok {
switch msiSecret.msiType {
case msiTypeAppServiceV20170901:
req.Method = http.MethodGet
req.Header.Set("secret", os.Getenv(msiSecretEnv))
break
case msiTypeCloudShell:
req.Header.Set("Metadata", "true")
data := url.Values{}
data.Set("resource", spt.inner.Resource)
if spt.inner.ClientID != "" {
data.Set("client_id", spt.inner.ClientID)
} else if msiSecret.clientResourceID != "" {
data.Set("msi_res_id", msiSecret.clientResourceID)
}
req.Body = ioutil.NopCloser(strings.NewReader(data.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
break
case msiTypeIMDS:
req.Method = http.MethodGet
req.Header.Set("Metadata", "true")
break
}
logger.Instance.WriteRequest(req, logger.Filter{Body: authBodyFilter})
resp, err = retryForIMDS(spt.sender, req, spt.MaxMSIRefreshAttempts)
} else {
v := url.Values{}
v.Set("client_id", spt.inner.ClientID)
v.Set("resource", resource)
@@ -892,32 +1020,26 @@ func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource
req.ContentLength = int64(len(s))
req.Header.Set(contentType, mimeTypeFormPost)
req.Body = body
}
if _, ok := spt.inner.Secret.(*ServicePrincipalMSISecret); ok {
req.Method = http.MethodGet
req.Header.Set(metadataHeader, "true")
}
var resp *http.Response
if isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
resp, err = retryForIMDS(spt.sender, req, spt.MaxMSIRefreshAttempts)
} else {
logger.Instance.WriteRequest(req, logger.Filter{Body: authBodyFilter})
resp, err = spt.sender.Do(req)
}
// don't return a TokenRefreshError here; this will allow retry logic to apply
if err != nil {
// don't return a TokenRefreshError here; this will allow retry logic to apply
return fmt.Errorf("adal: Failed to execute the refresh request. Error = '%v'", err)
} else if resp == nil {
return fmt.Errorf("adal: received nil response and error")
}
logger.Instance.WriteResponse(resp, logger.Filter{Body: authBodyFilter})
defer resp.Body.Close()
rb, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
if err != nil {
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body: %v", resp.StatusCode, err), resp)
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body: %v Endpoint %s", resp.StatusCode, err, req.URL.String()), resp)
}
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Response body: %s", resp.StatusCode, string(rb)), resp)
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Response body: %s Endpoint %s", resp.StatusCode, string(rb), req.URL.String()), resp)
}
// for the following error cases don't return a TokenRefreshError. the operation succeeded
@@ -930,15 +1052,60 @@ func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource
if len(strings.Trim(string(rb), " ")) == 0 {
return fmt.Errorf("adal: Empty service principal token received during refresh")
}
var token Token
token := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
// AAD returns expires_in as a string, ADFS returns it as an int
ExpiresIn json.Number `json:"expires_in"`
// expires_on can be in two formats, a UTC time stamp or the number of seconds.
ExpiresOn string `json:"expires_on"`
NotBefore json.Number `json:"not_before"`
Resource string `json:"resource"`
Type string `json:"token_type"`
}{}
// return a TokenRefreshError in the follow error cases as the token is in an unexpected format
err = json.Unmarshal(rb, &token)
if err != nil {
return fmt.Errorf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb))
return newTokenRefreshError(fmt.Sprintf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb)), resp)
}
expiresOn := json.Number("")
// ADFS doesn't include the expires_on field
if token.ExpiresOn != "" {
if expiresOn, err = parseExpiresOn(token.ExpiresOn); err != nil {
return newTokenRefreshError(fmt.Sprintf("adal: failed to parse expires_on: %v value '%s'", err, token.ExpiresOn), resp)
}
}
spt.inner.Token.AccessToken = token.AccessToken
spt.inner.Token.RefreshToken = token.RefreshToken
spt.inner.Token.ExpiresIn = token.ExpiresIn
spt.inner.Token.ExpiresOn = expiresOn
spt.inner.Token.NotBefore = token.NotBefore
spt.inner.Token.Resource = token.Resource
spt.inner.Token.Type = token.Type
spt.inner.Token = token
return spt.InvokeRefreshCallbacks(spt.inner.Token)
}
return spt.InvokeRefreshCallbacks(token)
// converts expires_on to the number of seconds
func parseExpiresOn(s string) (json.Number, error) {
// convert the expiration date to the number of seconds from now
timeToDuration := func(t time.Time) json.Number {
dur := t.Sub(time.Now().UTC())
return json.Number(strconv.FormatInt(int64(dur.Round(time.Second).Seconds()), 10))
}
if _, err := strconv.ParseInt(s, 10, 64); err == nil {
// this is the number of seconds case, no conversion required
return json.Number(s), nil
} else if eo, err := time.Parse(expiresOnDateFormatPM, s); err == nil {
return timeToDuration(eo), nil
} else if eo, err := time.Parse(expiresOnDateFormat, s); err == nil {
return timeToDuration(eo), nil
} else {
// unknown format
return json.Number(""), err
}
}
// retry logic specific to retrieving a token from the IMDS endpoint
@@ -972,6 +1139,11 @@ func retryForIMDS(sender Sender, req *http.Request, maxAttempts int) (resp *http
attempt := 0
delay := time.Duration(0)
// maxAttempts is user-specified, ensure that its value is greater than zero else no request will be made
if maxAttempts < 1 {
maxAttempts = defaultMaxMSIRefreshAttempts
}
for attempt < maxAttempts {
if resp != nil && resp.Body != nil {
io.Copy(ioutil.Discard, resp.Body)
@@ -1064,46 +1236,6 @@ func (mt *MultiTenantServicePrincipalToken) AuxiliaryOAuthTokens() []string {
return tokens
}
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %v", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %v", err)
}
}
return nil
}
// RefreshWithContext obtains a fresh token for the Service Principal.
func (mt *MultiTenantServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.RefreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %v", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %v", err)
}
}
return nil
}
// RefreshExchangeWithContext refreshes the token, but for a different resource.
func (mt *MultiTenantServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
if err := mt.PrimaryToken.RefreshExchangeWithContext(ctx, resource); err != nil {
return fmt.Errorf("failed to refresh primary token: %v", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshExchangeWithContext(ctx, resource); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %v", err)
}
}
return nil
}
// NewMultiTenantServicePrincipalToken creates a new MultiTenantServicePrincipalToken with the specified credentials and resource.
func NewMultiTenantServicePrincipalToken(multiTenantCfg MultiTenantOAuthConfig, clientID string, secret string, resource string) (*MultiTenantServicePrincipalToken, error) {
if err := validateStringParam(clientID, "clientID"); err != nil {
@@ -1133,3 +1265,64 @@ func NewMultiTenantServicePrincipalToken(multiTenantCfg MultiTenantOAuthConfig,
}
return &m, nil
}
// NewMultiTenantServicePrincipalTokenFromCertificate creates a new MultiTenantServicePrincipalToken with the specified certificate credentials and resource.
func NewMultiTenantServicePrincipalTokenFromCertificate(multiTenantCfg MultiTenantOAuthConfig, clientID string, certificate *x509.Certificate, privateKey *rsa.PrivateKey, resource string) (*MultiTenantServicePrincipalToken, error) {
if err := validateStringParam(clientID, "clientID"); err != nil {
return nil, err
}
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
if certificate == nil {
return nil, fmt.Errorf("parameter 'certificate' cannot be nil")
}
if privateKey == nil {
return nil, fmt.Errorf("parameter 'privateKey' cannot be nil")
}
auxTenants := multiTenantCfg.AuxiliaryTenants()
m := MultiTenantServicePrincipalToken{
AuxiliaryTokens: make([]*ServicePrincipalToken, len(auxTenants)),
}
primary, err := NewServicePrincipalTokenWithSecret(
*multiTenantCfg.PrimaryTenant(),
clientID,
resource,
&ServicePrincipalCertificateSecret{
PrivateKey: privateKey,
Certificate: certificate,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for primary tenant: %v", err)
}
m.PrimaryToken = primary
for i := range auxTenants {
aux, err := NewServicePrincipalTokenWithSecret(
*auxTenants[i],
clientID,
resource,
&ServicePrincipalCertificateSecret{
PrivateKey: privateKey,
Certificate: certificate,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for auxiliary tenant: %v", err)
}
m.AuxiliaryTokens[i] = aux
}
return &m, nil
}
// MSIAvailable returns true if the MSI endpoint is available for authentication.
func MSIAvailable(ctx context.Context, s Sender) bool {
if s == nil {
s = sender()
}
resp, err := getMSIEndpoint(ctx, s)
if err == nil {
resp.Body.Close()
}
return err == nil
}

View File

@@ -0,0 +1,76 @@
//go:build go1.13
// +build go1.13
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adal
import (
"context"
"fmt"
"net/http"
"time"
)
func getMSIEndpoint(ctx context.Context, sender Sender) (*http.Response, error) {
tempCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// http.NewRequestWithContext() was added in Go 1.13
req, _ := http.NewRequestWithContext(tempCtx, http.MethodGet, msiEndpoint, nil)
q := req.URL.Query()
q.Add("api-version", msiAPIVersion)
req.URL.RawQuery = q.Encode()
return sender.Do(req)
}
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %w", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %w", err)
}
}
return nil
}
// RefreshWithContext obtains a fresh token for the Service Principal.
func (mt *MultiTenantServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.RefreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %w", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %w", err)
}
}
return nil
}
// RefreshExchangeWithContext refreshes the token, but for a different resource.
func (mt *MultiTenantServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
if err := mt.PrimaryToken.RefreshExchangeWithContext(ctx, resource); err != nil {
return fmt.Errorf("failed to refresh primary token: %w", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshExchangeWithContext(ctx, resource); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,75 @@
//go:build !go1.13
// +build !go1.13
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adal
import (
"context"
"net/http"
"time"
)
func getMSIEndpoint(ctx context.Context, sender Sender) (*http.Response, error) {
tempCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, _ := http.NewRequest(http.MethodGet, msiEndpoint, nil)
req = req.WithContext(tempCtx)
q := req.URL.Query()
q.Add("api-version", msiAPIVersion)
req.URL.RawQuery = q.Encode()
return sender.Do(req)
}
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil {
return err
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.EnsureFreshWithContext(ctx); err != nil {
return err
}
}
return nil
}
// RefreshWithContext obtains a fresh token for the Service Principal.
func (mt *MultiTenantServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.RefreshWithContext(ctx); err != nil {
return err
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshWithContext(ctx); err != nil {
return err
}
}
return nil
}
// RefreshExchangeWithContext refreshes the token, but for a different resource.
func (mt *MultiTenantServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
if err := mt.PrimaryToken.RefreshExchangeWithContext(ctx, resource); err != nil {
return err
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.RefreshExchangeWithContext(ctx, resource); err != nil {
return err
}
}
return nil
}

View File

@@ -138,6 +138,11 @@ func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator {
}
}
// TokenProvider returns OAuthTokenProvider so that it can be used for authorization outside the REST.
func (ba *BearerAuthorizer) TokenProvider() adal.OAuthTokenProvider {
return ba.tokenProvider
}
// BearerAuthorizerCallbackFunc is the authentication callback signature.
type BearerAuthorizerCallbackFunc func(tenantID, resource string) (*BearerAuthorizer, error)
@@ -294,18 +299,24 @@ type MultiTenantServicePrincipalTokenAuthorizer interface {
// NewMultiTenantServicePrincipalTokenAuthorizer crates a BearerAuthorizer using the given token provider
func NewMultiTenantServicePrincipalTokenAuthorizer(tp adal.MultitenantOAuthTokenProvider) MultiTenantServicePrincipalTokenAuthorizer {
return &multiTenantSPTAuthorizer{tp: tp}
return NewMultiTenantBearerAuthorizer(tp)
}
type multiTenantSPTAuthorizer struct {
// MultiTenantBearerAuthorizer implements bearer authorization across multiple tenants.
type MultiTenantBearerAuthorizer struct {
tp adal.MultitenantOAuthTokenProvider
}
// NewMultiTenantBearerAuthorizer creates a MultiTenantBearerAuthorizer using the given token provider.
func NewMultiTenantBearerAuthorizer(tp adal.MultitenantOAuthTokenProvider) *MultiTenantBearerAuthorizer {
return &MultiTenantBearerAuthorizer{tp: tp}
}
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header using the
// primary token along with the auxiliary authorization header using the auxiliary tokens.
//
// By default, the token will be automatically refreshed through the Refresher interface.
func (mt multiTenantSPTAuthorizer) WithAuthorization() PrepareDecorator {
func (mt *MultiTenantBearerAuthorizer) WithAuthorization() PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
@@ -331,7 +342,12 @@ func (mt multiTenantSPTAuthorizer) WithAuthorization() PrepareDecorator {
for i := range auxTokens {
auxTokens[i] = fmt.Sprintf("Bearer %s", auxTokens[i])
}
return Prepare(r, WithHeader(headerAuxAuthorization, strings.Join(auxTokens, "; ")))
return Prepare(r, WithHeader(headerAuxAuthorization, strings.Join(auxTokens, ", ")))
})
}
}
// TokenProvider returns the underlying MultitenantOAuthTokenProvider for this authorizer.
func (mt *MultiTenantBearerAuthorizer) TokenProvider() adal.MultitenantOAuthTokenProvider {
return mt.tp
}

View File

@@ -54,13 +54,12 @@ func (sas *SASTokenAuthorizer) WithAuthorization() PrepareDecorator {
return r, err
}
if r.URL.RawQuery != "" {
r.URL.RawQuery = fmt.Sprintf("%s&%s", r.URL.RawQuery, sas.sasToken)
} else {
if r.URL.RawQuery == "" {
r.URL.RawQuery = sas.sasToken
} else if !strings.Contains(r.URL.RawQuery, sas.sasToken) {
r.URL.RawQuery = fmt.Sprintf("%s&%s", r.URL.RawQuery, sas.sasToken)
}
r.RequestURI = r.URL.String()
return Prepare(r)
})
}

View File

@@ -152,6 +152,9 @@ func buildCanonicalizedResource(accountName, uri string, keyType SharedKeyType)
// the resource's URI should be encoded exactly as it is in the URI.
// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx
cr.WriteString(u.EscapedPath())
} else {
// a slash is required to indicate the root path
cr.WriteString("/")
}
params, err := url.ParseQuery(u.RawQuery)

View File

@@ -26,6 +26,7 @@ import (
"time"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/logger"
"github.com/Azure/go-autorest/tracing"
)
@@ -42,6 +43,52 @@ const (
var pollingCodes = [...]int{http.StatusNoContent, http.StatusAccepted, http.StatusCreated, http.StatusOK}
// FutureAPI contains the set of methods on the Future type.
type FutureAPI interface {
// Response returns the last HTTP response.
Response() *http.Response
// Status returns the last status message of the operation.
Status() string
// PollingMethod returns the method used to monitor the status of the asynchronous operation.
PollingMethod() PollingMethodType
// DoneWithContext queries the service to see if the operation has completed.
DoneWithContext(context.Context, autorest.Sender) (bool, error)
// GetPollingDelay returns a duration the application should wait before checking
// the status of the asynchronous request and true; this value is returned from
// the service via the Retry-After response header. If the header wasn't returned
// then the function returns the zero-value time.Duration and false.
GetPollingDelay() (time.Duration, bool)
// WaitForCompletionRef will return when one of the following conditions is met: the long
// running operation has completed, the provided context is cancelled, or the client's
// polling duration has been exceeded. It will retry failed polling attempts based on
// the retry value defined in the client up to the maximum retry attempts.
// If no deadline is specified in the context then the client.PollingDuration will be
// used to determine if a default deadline should be used.
// If PollingDuration is greater than zero the value will be used as the context's timeout.
// If PollingDuration is zero then no default deadline will be used.
WaitForCompletionRef(context.Context, autorest.Client) error
// MarshalJSON implements the json.Marshaler interface.
MarshalJSON() ([]byte, error)
// MarshalJSON implements the json.Unmarshaler interface.
UnmarshalJSON([]byte) error
// PollingURL returns the URL used for retrieving the status of the long-running operation.
PollingURL() string
// GetResult should be called once polling has completed successfully.
// It makes the final GET call to retrieve the resultant payload.
GetResult(autorest.Sender) (*http.Response, error)
}
var _ FutureAPI = (*Future)(nil)
// Future provides a mechanism to access the status and results of an asynchronous request.
// Since futures are stateful they should be passed by value to avoid race conditions.
type Future struct {
@@ -167,7 +214,14 @@ func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Clien
cancelCtx, cancel = context.WithTimeout(ctx, d)
defer cancel()
}
// if the initial response has a Retry-After, sleep for the specified amount of time before starting to poll
if delay, ok := f.GetPollingDelay(); ok {
logger.Instance.Writeln(logger.LogInfo, "WaitForCompletionRef: initial polling delay")
if delayElapsed := autorest.DelayForBackoff(delay, 0, cancelCtx.Done()); !delayElapsed {
err = cancelCtx.Err()
return
}
}
done, err := f.DoneWithContext(ctx, client)
for attempts := 0; !done; done, err = f.DoneWithContext(ctx, client) {
if attempts >= client.RetryAttempts {
@@ -182,12 +236,14 @@ func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Clien
var ok bool
delay, ok = f.GetPollingDelay()
if !ok {
logger.Instance.Writeln(logger.LogInfo, "WaitForCompletionRef: Using client polling delay")
delay = client.PollingDelay
}
} else {
// there was an error polling for status so perform exponential
// back-off based on the number of attempts using the client's retry
// duration. update attempts after delayAttempt to avoid off-by-one.
logger.Instance.Writef(logger.LogError, "WaitForCompletionRef: %s\n", err)
delayAttempt = attempts
delay = client.RetryDuration
attempts++
@@ -407,12 +463,12 @@ func (pt *pollingTrackerBase) updateRawBody() error {
if err != nil {
return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to read response body")
}
// put the body back so it's available to other callers
pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b))
// observed in 204 responses over HTTP/2.0; the content length is -1 but body is empty
if len(b) == 0 {
return nil
}
// put the body back so it's available to other callers
pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b))
if err = json.Unmarshal(b, &pt.rawBody); err != nil {
return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to unmarshal response body")
}
@@ -460,7 +516,12 @@ func (pt *pollingTrackerBase) updateErrorFromResponse() {
re := respErr{}
defer pt.resp.Body.Close()
var b []byte
if b, err = ioutil.ReadAll(pt.resp.Body); err != nil || len(b) == 0 {
if b, err = ioutil.ReadAll(pt.resp.Body); err != nil {
goto Default
}
// put the body back so it's available to other callers
pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b))
if len(b) == 0 {
goto Default
}
if err = json.Unmarshal(b, &re); err != nil {

View File

@@ -0,0 +1,152 @@
# NOTE: This module will go out of support by March 31, 2023. For authenticating with Azure AD, use module [azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity) instead. For help migrating from `auth` to `azidentiy` please consult the [migration guide](https://aka.ms/azsdk/go/identity/migration). General information about the retirement of this and other legacy modules can be found [here](https://azure.microsoft.com/updates/support-for-azure-sdk-libraries-that-do-not-conform-to-our-current-azure-sdk-guidelines-will-be-retired-as-of-31-march-2023/).
## Authentication
Typical SDK operations must be authenticated and authorized. The `autorest.Authorizer`
interface allows use of any auth style in requests, such as inserting an OAuth2
Authorization header and bearer token received from Azure AD.
The SDK itself provides a simple way to get an authorizer which first checks
for OAuth client credentials in environment variables and then falls back to
Azure's [Managed Service Identity]() when available, e.g. when on an Azure
VM. The following snippet from [the previous section](#use) demonstrates
this helper.
```go
import "github.com/Azure/go-autorest/autorest/azure/auth"
// create a VirtualNetworks client
vnetClient := network.NewVirtualNetworksClient("<subscriptionID>")
// create an authorizer from env vars or Azure Managed Service Idenity
authorizer, err := auth.NewAuthorizerFromEnvironment()
if err != nil {
handle(err)
}
vnetClient.Authorizer = authorizer
// call the VirtualNetworks CreateOrUpdate API
vnetClient.CreateOrUpdate(context.Background(),
// ...
```
The following environment variables help determine authentication configuration:
- `AZURE_ENVIRONMENT`: Specifies the Azure Environment to use. If not set, it
defaults to `AzurePublicCloud`. Not applicable to authentication with Managed
Service Identity (MSI).
- `AZURE_AD_RESOURCE`: Specifies the AAD resource ID to use. If not set, it
defaults to `ResourceManagerEndpoint` for operations with Azure Resource
Manager. You can also choose an alternate resource programmatically with
`auth.NewAuthorizerFromEnvironmentWithResource(resource string)`.
### More Authentication Details
The previous is the first and most recommended of several authentication
options offered by the SDK because it allows seamless use of both service
principals and [Azure Managed Service Identity][]. Other options are listed
below.
> Note: If you need to create a new service principal, run `az ad sp create-for-rbac -n "<app_name>"` in the
> [azure-cli](https://github.com/Azure/azure-cli). See [these
> docs](https://docs.microsoft.com/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest)
> for more info. Copy the new principal's ID, secret, and tenant ID for use in
> your app, or consider the `--sdk-auth` parameter for serialized output.
[azure managed service identity]: https://docs.microsoft.com/azure/active-directory/msi-overview
- The `auth.NewAuthorizerFromEnvironment()` described above creates an authorizer
from the first available of the following configuration:
1. **Client Credentials**: Azure AD Application ID and Secret.
- `AZURE_TENANT_ID`: Specifies the Tenant to which to authenticate.
- `AZURE_CLIENT_ID`: Specifies the app client ID to use.
- `AZURE_CLIENT_SECRET`: Specifies the app secret to use.
2. **Client Certificate**: Azure AD Application ID and X.509 Certificate.
- `AZURE_TENANT_ID`: Specifies the Tenant to which to authenticate.
- `AZURE_CLIENT_ID`: Specifies the app client ID to use.
- `AZURE_CERTIFICATE_PATH`: Specifies the certificate Path to use.
- `AZURE_CERTIFICATE_PASSWORD`: Specifies the certificate password to use.
3. **Resource Owner Password**: Azure AD User and Password. This grant type is *not
recommended*, use device login instead if you need interactive login.
- `AZURE_TENANT_ID`: Specifies the Tenant to which to authenticate.
- `AZURE_CLIENT_ID`: Specifies the app client ID to use.
- `AZURE_USERNAME`: Specifies the username to use.
- `AZURE_PASSWORD`: Specifies the password to use.
4. **Azure Managed Service Identity**: Delegate credential management to the
platform. Requires that code is running in Azure, e.g. on a VM. All
configuration is handled by Azure. See [Azure Managed Service
Identity](https://docs.microsoft.com/azure/active-directory/msi-overview)
for more details.
- The `auth.NewAuthorizerFromFile()` method creates an authorizer using
credentials from an auth file created by the [Azure CLI][]. Follow these
steps to utilize:
1. Create a service principal and output an auth file using `az ad sp create-for-rbac --sdk-auth > client_credentials.json`.
2. Set environment variable `AZURE_AUTH_LOCATION` to the path of the saved
output file.
3. Use the authorizer returned by `auth.NewAuthorizerFromFile()` in your
client as described above.
- The `auth.NewAuthorizerFromCLI()` method creates an authorizer which
uses [Azure CLI][] to obtain its credentials.
The default audience being requested is `https://management.azure.com` (Azure ARM API).
To specify your own audience, export `AZURE_AD_RESOURCE` as an evironment variable.
This is read by `auth.NewAuthorizerFromCLI()` and passed to Azure CLI to acquire the access token.
For example, to request an access token for Azure Key Vault, export
```
AZURE_AD_RESOURCE="https://vault.azure.net"
```
- `auth.NewAuthorizerFromCLIWithResource(AUDIENCE_URL_OR_APPLICATION_ID)` - this method is self contained and does
not require exporting environment variables. For example, to request an access token for Azure Key Vault:
```
auth.NewAuthorizerFromCLIWithResource("https://vault.azure.net")
```
To use `NewAuthorizerFromCLI()` or `NewAuthorizerFromCLIWithResource()`, follow these steps:
1. Install [Azure CLI v2.0.12](https://docs.microsoft.com/cli/azure/install-azure-cli) or later. Upgrade earlier versions.
2. Use `az login` to sign in to Azure.
If you receive an error, use `az account get-access-token` to verify access.
If Azure CLI is not installed to the default directory, you may receive an error
reporting that `az` cannot be found.
Use the `AzureCLIPath` environment variable to define the Azure CLI installation folder.
If you are signed in to Azure CLI using multiple accounts or your account has
access to multiple subscriptions, you need to specify the specific subscription
to be used. To do so, use:
```
az account set --subscription <subscription-id>
```
To verify the current account settings, use:
```
az account list
```
[azure cli]: https://github.com/Azure/azure-cli
- Finally, you can use OAuth's [Device Flow][] by calling
`auth.NewDeviceFlowConfig()` and extracting the Authorizer as follows:
```go
config := auth.NewDeviceFlowConfig(clientID, tenantID)
a, err := config.Authorizer()
```
[device flow]: https://oauth.net/2/device-flow/

View File

@@ -16,8 +16,7 @@ package auth
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"context"
"encoding/binary"
"encoding/json"
"errors"
@@ -32,8 +31,8 @@ import (
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/cli"
"github.com/Azure/go-autorest/logger"
"github.com/dimchansky/utfbom"
"golang.org/x/crypto/pkcs12"
)
// The possible keys in the Values map.
@@ -63,6 +62,7 @@ const (
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironment() (autorest.Authorizer, error) {
logger.Instance.Writeln(logger.LogInfo, "NewAuthorizerFromEnvironment() determining authentication mechanism")
settings, err := GetSettingsFromEnvironment()
if err != nil {
return nil, err
@@ -76,6 +76,7 @@ func NewAuthorizerFromEnvironment() (autorest.Authorizer, error) {
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironmentWithResource(resource string) (autorest.Authorizer, error) {
logger.Instance.Writeln(logger.LogInfo, "NewAuthorizerFromEnvironmentWithResource() determining authentication mechanism")
settings, err := GetSettingsFromEnvironment()
if err != nil {
return nil, err
@@ -125,6 +126,7 @@ func (settings EnvironmentSettings) GetSubscriptionID() string {
// adds the specified environment variable value to the Values map if it exists
func (settings EnvironmentSettings) setValue(key string) {
if v := os.Getenv(key); v != "" {
logger.Instance.Writef(logger.LogInfo, "GetSettingsFromEnvironment() found environment var %s\n", key)
settings.Values[key] = v
}
}
@@ -141,6 +143,7 @@ func (settings EnvironmentSettings) getClientAndTenant() (string, string) {
func (settings EnvironmentSettings) GetClientCredentials() (ClientCredentialsConfig, error) {
secret := settings.Values[ClientSecret]
if secret == "" {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetClientCredentials() missing client secret")
return ClientCredentialsConfig{}, errors.New("missing client secret")
}
clientID, tenantID := settings.getClientAndTenant()
@@ -161,6 +164,7 @@ func (settings EnvironmentSettings) GetClientCredentials() (ClientCredentialsCon
func (settings EnvironmentSettings) GetClientCertificate() (ClientCertificateConfig, error) {
certPath := settings.Values[CertificatePath]
if certPath == "" {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetClientCertificate() missing certificate path")
return ClientCertificateConfig{}, errors.New("missing certificate path")
}
certPwd := settings.Values[CertificatePassword]
@@ -177,6 +181,7 @@ func (settings EnvironmentSettings) GetUsernamePassword() (UsernamePasswordConfi
username := settings.Values[Username]
password := settings.Values[Password]
if username == "" || password == "" {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetUsernamePassword() missing username and/or password")
return UsernamePasswordConfig{}, errors.New("missing username/password")
}
clientID, tenantID := settings.getClientAndTenant()
@@ -211,35 +216,55 @@ func (settings EnvironmentSettings) GetDeviceFlow() DeviceFlowConfig {
func (settings EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
//1.Client Credentials
if c, e := settings.GetClientCredentials(); e == nil {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using client secret credentials")
return c.Authorizer()
}
//2. Client Certificate
if c, e := settings.GetClientCertificate(); e == nil {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using client certificate credentials")
return c.Authorizer()
}
//3. Username Password
if c, e := settings.GetUsernamePassword(); e == nil {
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using user name/password credentials")
return c.Authorizer()
}
// 4. MSI
if !adal.MSIAvailable(context.Background(), nil) {
return nil, errors.New("MSI not available")
}
logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using MSI authentication")
return settings.GetMSI().Authorizer()
}
// NewAuthorizerFromFile creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
func NewAuthorizerFromFile(baseURI string) (autorest.Authorizer, error) {
// The path to the configuration file must be specified in the AZURE_AUTH_LOCATION environment variable.
// resourceBaseURI - used to determine the resource type
func NewAuthorizerFromFile(resourceBaseURI string) (autorest.Authorizer, error) {
settings, err := GetSettingsFromFile()
if err != nil {
return nil, err
}
if a, err := settings.ClientCredentialsAuthorizer(baseURI); err == nil {
return settings.GetAuthorizer(resourceBaseURI)
}
// GetAuthorizer create an Authorizer in the following order.
// 1. Client credentials
// 2. Client certificate
// resourceBaseURI - used to determine the resource type
func (settings FileSettings) GetAuthorizer(resourceBaseURI string) (autorest.Authorizer, error) {
if resourceBaseURI == "" {
resourceBaseURI = azure.PublicCloud.ServiceManagementEndpoint
}
if a, err := settings.ClientCredentialsAuthorizer(resourceBaseURI); err == nil {
return a, err
}
if a, err := settings.ClientCertificateAuthorizer(baseURI); err == nil {
if a, err := settings.ClientCertificateAuthorizer(resourceBaseURI); err == nil {
return a, err
}
return nil, errors.New("auth file missing client and certificate credentials")
@@ -248,6 +273,7 @@ func NewAuthorizerFromFile(baseURI string) (autorest.Authorizer, error) {
// NewAuthorizerFromFileWithResource creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
// The path to the configuration file must be specified in the AZURE_AUTH_LOCATION environment variable.
func NewAuthorizerFromFileWithResource(resource string) (autorest.Authorizer, error) {
s, err := GetSettingsFromFile()
if err != nil {
@@ -466,7 +492,7 @@ func decode(b []byte) ([]byte, error) {
}
func (settings FileSettings) getResourceForToken(baseURI string) (string, error) {
// Compare dafault base URI from the SDK to the endpoints from the public cloud
// Compare default base URI from the SDK to the endpoints from the public cloud
// Base URI and token resource are the same string. This func finds the authentication
// file field that matches the SDK base URI. The SDK defines the public cloud
// endpoint as its default base URI
@@ -544,7 +570,7 @@ func NewDeviceFlowConfig(clientID string, tenantID string) DeviceFlowConfig {
}
}
//AuthorizerConfig provides an authorizer from the configuration provided.
// AuthorizerConfig provides an authorizer from the configuration provided.
type AuthorizerConfig interface {
Authorizer() (autorest.Authorizer, error)
}
@@ -599,6 +625,7 @@ type ClientCertificateConfig struct {
CertificatePath string
CertificatePassword string
TenantID string
AuxTenants []string
AADEndpoint string
Resource string
}
@@ -613,20 +640,44 @@ func (ccc ClientCertificateConfig) ServicePrincipalToken() (*adal.ServicePrincip
if err != nil {
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
}
certificate, rsaPrivateKey, err := decodePkcs12(certData, ccc.CertificatePassword)
certificate, rsaPrivateKey, err := adal.DecodePfxCertificateData(certData, ccc.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
}
// MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client certificate.
func (ccc ClientCertificateConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) {
oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.OAuthOptions{})
if err != nil {
return nil, err
}
certData, err := ioutil.ReadFile(ccc.CertificatePath)
if err != nil {
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
}
certificate, rsaPrivateKey, err := adal.DecodePfxCertificateData(certData, ccc.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
return adal.NewMultiTenantServicePrincipalTokenFromCertificate(oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
}
// Authorizer gets an authorizer object from client certificate.
func (ccc ClientCertificateConfig) Authorizer() (autorest.Authorizer, error) {
spToken, err := ccc.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
if len(ccc.AuxTenants) == 0 {
spToken, err := ccc.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
return autorest.NewBearerAuthorizer(spToken), nil
mtSPT, err := ccc.MultiTenantServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get multitenant SPT from certificate auth: %v", err)
}
return autorest.NewMultiTenantServicePrincipalTokenAuthorizer(mtSPT), nil
}
// DeviceFlowConfig provides the options to get a bearer authorizer using device flow authentication.
@@ -665,20 +716,6 @@ func (dfc DeviceFlowConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken
return adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, dfc.ClientID, dfc.Resource, *token)
}
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
if err != nil {
return nil, nil, err
}
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
if !isRsaKey {
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
}
return certificate, rsaPrivateKey, nil
}
// UsernamePasswordConfig provides the options to get a bearer authorizer from a username and a password.
type UsernamePasswordConfig struct {
ClientID string
@@ -713,25 +750,23 @@ type MSIConfig struct {
ClientID string
}
// ServicePrincipalToken creates a ServicePrincipalToken from MSI.
func (mc MSIConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
spToken, err := adal.NewServicePrincipalTokenFromManagedIdentity(mc.Resource, &adal.ManagedIdentityOptions{
ClientID: mc.ClientID,
})
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
return spToken, nil
}
// Authorizer gets the authorizer from MSI.
func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
msiEndpoint, err := adal.GetMSIEndpoint()
spToken, err := mc.ServicePrincipalToken()
if err != nil {
return nil, err
}
var spToken *adal.ServicePrincipalToken
if mc.ClientID == "" {
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
} else {
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
}
}
return autorest.NewBearerAuthorizer(spToken), nil
}

View File

@@ -1,3 +1,4 @@
//go:build modhack
// +build modhack
package auth
@@ -16,9 +17,9 @@ package auth
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest/autorest"
import _ "github.com/Azure/go-autorest"

View File

@@ -37,6 +37,9 @@ const (
// should be included in the response.
HeaderReturnClientID = "x-ms-return-client-request-id"
// HeaderContentType is the type of the content in the HTTP response.
HeaderContentType = "Content-Type"
// HeaderRequestID is the Azure extension header of the service generated request ID returned
// in the response.
HeaderRequestID = "x-ms-request-id"
@@ -65,7 +68,7 @@ func (se ServiceError) Error() string {
if err != nil {
result += fmt.Sprintf(" Details=%v", se.Details)
}
result += fmt.Sprintf(" Details=%v", string(d))
result += fmt.Sprintf(" Details=%s", d)
}
if se.InnerError != nil {
@@ -73,7 +76,7 @@ func (se ServiceError) Error() string {
if err != nil {
result += fmt.Sprintf(" InnerError=%v", se.InnerError)
}
result += fmt.Sprintf(" InnerError=%v", string(d))
result += fmt.Sprintf(" InnerError=%s", d)
}
if se.AdditionalInfo != nil {
@@ -81,7 +84,7 @@ func (se ServiceError) Error() string {
if err != nil {
result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo)
}
result += fmt.Sprintf(" AdditionalInfo=%v", string(d))
result += fmt.Sprintf(" AdditionalInfo=%s", d)
}
return result
@@ -89,54 +92,85 @@ func (se ServiceError) Error() string {
// UnmarshalJSON implements the json.Unmarshaler interface for the ServiceError type.
func (se *ServiceError) UnmarshalJSON(b []byte) error {
// per the OData v4 spec the details field must be an array of JSON objects.
// unfortunately not all services adhear to the spec and just return a single
// object instead of an array with one object. so we have to perform some
// shenanigans to accommodate both cases.
// http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
type serviceError1 struct {
type serviceErrorInternal struct {
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details []map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
Target *string `json:"target,omitempty"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo,omitempty"`
// not all services conform to the OData v4 spec.
// the following fields are where we've seen discrepancies
// spec calls for []map[string]interface{} but have seen map[string]interface{}
Details interface{} `json:"details,omitempty"`
// spec calls for map[string]interface{} but have seen []map[string]interface{} and string
InnerError interface{} `json:"innererror,omitempty"`
}
type serviceError2 struct {
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
sei := serviceErrorInternal{}
if err := json.Unmarshal(b, &sei); err != nil {
return err
}
se1 := serviceError1{}
err := json.Unmarshal(b, &se1)
if err == nil {
se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError, se1.AdditionalInfo)
return nil
// copy the fields we know to be correct
se.AdditionalInfo = sei.AdditionalInfo
se.Code = sei.Code
se.Message = sei.Message
se.Target = sei.Target
// converts an []interface{} to []map[string]interface{}
arrayOfObjs := func(v interface{}) ([]map[string]interface{}, bool) {
arrayOf, ok := v.([]interface{})
if !ok {
return nil, false
}
final := []map[string]interface{}{}
for _, item := range arrayOf {
as, ok := item.(map[string]interface{})
if !ok {
return nil, false
}
final = append(final, as)
}
return final, true
}
se2 := serviceError2{}
err = json.Unmarshal(b, &se2)
if err == nil {
se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError, se2.AdditionalInfo)
se.Details = append(se.Details, se2.Details)
return nil
}
return err
}
// convert the remaining fields, falling back to raw JSON if necessary
func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}, additional []map[string]interface{}) {
se.Code = code
se.Message = message
se.Target = target
se.Details = details
se.InnerError = inner
se.AdditionalInfo = additional
if c, ok := arrayOfObjs(sei.Details); ok {
se.Details = c
} else if c, ok := sei.Details.(map[string]interface{}); ok {
se.Details = []map[string]interface{}{c}
} else if sei.Details != nil {
// stuff into Details
se.Details = []map[string]interface{}{
{"raw": sei.Details},
}
}
if c, ok := sei.InnerError.(map[string]interface{}); ok {
se.InnerError = c
} else if c, ok := arrayOfObjs(sei.InnerError); ok {
// if there's only one error extract it
if len(c) == 1 {
se.InnerError = c[0]
} else {
// multiple errors, stuff them into the value
se.InnerError = map[string]interface{}{
"multi": c,
}
}
} else if c, ok := sei.InnerError.(string); ok {
se.InnerError = map[string]interface{}{"error": c}
} else if sei.InnerError != nil {
// stuff into InnerError
se.InnerError = map[string]interface{}{
"raw": sei.InnerError,
}
}
return nil
}
// RequestError describes an error response returned by Azure service.
@@ -171,8 +205,13 @@ type Resource struct {
ResourceName string
}
// String function returns a string in form of azureResourceID
func (r Resource) String() string {
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s", r.SubscriptionID, r.ResourceGroup, r.Provider, r.ResourceType, r.ResourceName)
}
// ParseResourceID parses a resource ID into a ResourceDetails struct.
// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#return-value-4.
// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions-resource?tabs=json#resourceid.
func ParseResourceID(resourceID string) (Resource, error) {
const resourceIDPatternText = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)`
@@ -296,22 +335,36 @@ func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
b, decodeErr := autorest.CopyAndDecode(encodedAs, resp.Body, &e)
resp.Body = ioutil.NopCloser(&b)
if decodeErr != nil {
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr)
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, decodeErr)
}
if e.ServiceError == nil {
// Check if error is unwrapped ServiceError
decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes()))
if err := decoder.Decode(&e.ServiceError); err != nil {
return err
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, err)
}
// for example, should the API return the literal value `null` as the response
if e.ServiceError == nil {
e.ServiceError = &ServiceError{
Code: "Unknown",
Message: "Unknown service error",
Details: []map[string]interface{}{
{
"HttpResponse.Body": b.String(),
},
},
}
}
}
if e.ServiceError.Message == "" {
if e.ServiceError != nil && e.ServiceError.Message == "" {
// if we're here it means the returned error wasn't OData v4 compliant.
// try to unmarshal the body in hopes of getting something.
rawBody := map[string]interface{}{}
decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes()))
if err := decoder.Decode(&rawBody); err != nil {
return err
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, err)
}
e.ServiceError = &ServiceError{

View File

@@ -1,3 +1,4 @@
//go:build modhack
// +build modhack
package cli
@@ -16,9 +17,9 @@ package cli
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest/autorest"
import _ "github.com/Azure/go-autorest"

View File

@@ -124,6 +124,83 @@ func LoadTokens(path string) ([]Token, error) {
// GetTokenFromCLI gets a token using Azure CLI 2.0 for local development scenarios.
func GetTokenFromCLI(resource string) (*Token, error) {
return GetTokenFromCLIWithParams(GetAccessTokenParams{Resource: resource})
}
// GetAccessTokenParams is the parameter struct of GetTokenFromCLIWithParams
type GetAccessTokenParams struct {
Resource string
ResourceType string
Subscription string
Tenant string
}
// GetTokenFromCLIWithParams gets a token using Azure CLI 2.0 for local development scenarios.
func GetTokenFromCLIWithParams(params GetAccessTokenParams) (*Token, error) {
cliCmd := GetAzureCLICommand()
cliCmd.Args = append(cliCmd.Args, "account", "get-access-token", "-o", "json")
if params.Resource != "" {
if err := validateParameter(params.Resource); err != nil {
return nil, err
}
cliCmd.Args = append(cliCmd.Args, "--resource", params.Resource)
}
if params.ResourceType != "" {
if err := validateParameter(params.ResourceType); err != nil {
return nil, err
}
cliCmd.Args = append(cliCmd.Args, "--resource-type", params.ResourceType)
}
if params.Subscription != "" {
if err := validateParameter(params.Subscription); err != nil {
return nil, err
}
cliCmd.Args = append(cliCmd.Args, "--subscription", params.Subscription)
}
if params.Tenant != "" {
if err := validateParameter(params.Tenant); err != nil {
return nil, err
}
cliCmd.Args = append(cliCmd.Args, "--tenant", params.Tenant)
}
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
if stderr.Len() > 0 {
return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String())
}
return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", err.Error())
}
tokenResponse := Token{}
err = json.Unmarshal(output, &tokenResponse)
if err != nil {
return nil, err
}
return &tokenResponse, err
}
func validateParameter(param string) error {
// Validate parameters, since it gets sent as a command line argument to Azure CLI
const invalidResourceErrorTemplate = "Parameter %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed."
match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", param)
if err != nil {
return err
}
if !match {
return fmt.Errorf(invalidResourceErrorTemplate, param)
}
return nil
}
// GetAzureCLICommand can be used to run arbitrary Azure CLI command
func GetAzureCLICommand() *exec.Cmd {
// This is the path that a developer can set to tell this class what the install path for Azure CLI is.
const azureCLIPath = "AzureCLIPath"
@@ -133,16 +210,6 @@ func GetTokenFromCLI(resource string) (*Token, error) {
// Default path for non-Windows.
const azureCLIDefaultPath = "/bin:/sbin:/usr/bin:/usr/local/bin"
// Validate resource, since it gets sent as a command line argument to Azure CLI
const invalidResourceErrorTemplate = "Resource %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed."
match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", resource)
if err != nil {
return nil, err
}
if !match {
return nil, fmt.Errorf(invalidResourceErrorTemplate, resource)
}
// Execute Azure CLI to get token
var cliCmd *exec.Cmd
if runtime.GOOS == "windows" {
@@ -155,21 +222,6 @@ func GetTokenFromCLI(resource string) (*Token, error) {
cliCmd.Env = os.Environ()
cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv(azureCLIPath), azureCLIDefaultPath))
}
cliCmd.Args = append(cliCmd.Args, "account", "get-access-token", "-o", "json", "--resource", resource)
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String())
}
tokenResponse := Token{}
err = json.Unmarshal(output, &tokenResponse)
if err != nil {
return nil, err
}
return &tokenResponse, err
return cliCmd
}

View File

@@ -34,8 +34,10 @@ const (
var environments = map[string]Environment{
"AZURECHINACLOUD": ChinaCloud,
"AZUREGERMANCLOUD": GermanCloud,
"AZURECLOUD": PublicCloud,
"AZUREPUBLICCLOUD": PublicCloud,
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud,
"AZUREUSGOVERNMENT": USGovernmentCloud,
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud, //TODO: deprecate
}
// ResourceIdentifier contains a set of Azure resource IDs.
@@ -45,7 +47,14 @@ type ResourceIdentifier struct {
Datalake string `json:"datalake"`
Batch string `json:"batch"`
OperationalInsights string `json:"operationalInsights"`
OSSRDBMS string `json:"ossRDBMS"`
Storage string `json:"storage"`
Synapse string `json:"synapse"`
ServiceBus string `json:"serviceBus"`
SQLDatabase string `json:"sqlDatabase"`
CosmosDB string `json:"cosmosDB"`
ManagedHSM string `json:"managedHSM"`
MicrosoftGraph string `json:"microsoftGraph"`
}
// Environment represents a set of endpoints for each of Azure's Clouds.
@@ -58,19 +67,28 @@ type Environment struct {
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
GalleryEndpoint string `json:"galleryEndpoint"`
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
ManagedHSMEndpoint string `json:"managedHSMEndpoint"`
GraphEndpoint string `json:"graphEndpoint"`
ServiceBusEndpoint string `json:"serviceBusEndpoint"`
BatchManagementEndpoint string `json:"batchManagementEndpoint"`
MicrosoftGraphEndpoint string `json:"microsoftGraphEndpoint"`
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
CosmosDBDNSSuffix string `json:"cosmosDBDNSSuffix"`
MariaDBDNSSuffix string `json:"mariaDBDNSSuffix"`
MySQLDatabaseDNSSuffix string `json:"mySqlDatabaseDNSSuffix"`
PostgresqlDatabaseDNSSuffix string `json:"postgresqlDatabaseDNSSuffix"`
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
ManagedHSMDNSSuffix string `json:"managedHSMDNSSuffix"`
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
CosmosDBDNSSuffix string `json:"cosmosDBDNSSuffix"`
TokenAudience string `json:"tokenAudience"`
APIManagementHostNameSuffix string `json:"apiManagementHostNameSuffix"`
SynapseEndpointSuffix string `json:"synapseEndpointSuffix"`
DatalakeSuffix string `json:"datalakeSuffix"`
ResourceIdentifiers ResourceIdentifier `json:"resourceIdentifiers"`
}
@@ -85,26 +103,42 @@ var (
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
GalleryEndpoint: "https://gallery.azure.com/",
KeyVaultEndpoint: "https://vault.azure.net/",
ManagedHSMEndpoint: "https://managedhsm.azure.net/",
GraphEndpoint: "https://graph.windows.net/",
ServiceBusEndpoint: "https://servicebus.windows.net/",
BatchManagementEndpoint: "https://batch.core.windows.net/",
MicrosoftGraphEndpoint: "https://graph.microsoft.com/",
StorageEndpointSuffix: "core.windows.net",
CosmosDBDNSSuffix: "documents.azure.com",
MariaDBDNSSuffix: "mariadb.database.azure.com",
MySQLDatabaseDNSSuffix: "mysql.database.azure.com",
PostgresqlDatabaseDNSSuffix: "postgres.database.azure.com",
SQLDatabaseDNSSuffix: "database.windows.net",
TrafficManagerDNSSuffix: "trafficmanager.net",
KeyVaultDNSSuffix: "vault.azure.net",
ManagedHSMDNSSuffix: "managedhsm.azure.net",
ServiceBusEndpointSuffix: "servicebus.windows.net",
ServiceManagementVMDNSSuffix: "cloudapp.net",
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
ContainerRegistryDNSSuffix: "azurecr.io",
CosmosDBDNSSuffix: "documents.azure.com",
TokenAudience: "https://management.azure.com/",
APIManagementHostNameSuffix: "azure-api.net",
SynapseEndpointSuffix: "dev.azuresynapse.net",
DatalakeSuffix: "azuredatalakestore.net",
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.windows.net/",
KeyVault: "https://vault.azure.net",
Datalake: "https://datalake.azure.net/",
Batch: "https://batch.core.windows.net/",
OperationalInsights: "https://api.loganalytics.io",
OSSRDBMS: "https://ossrdbms-aad.database.windows.net",
Storage: "https://storage.azure.com/",
Synapse: "https://dev.azuresynapse.net",
ServiceBus: "https://servicebus.azure.net/",
SQLDatabase: "https://database.windows.net/",
CosmosDB: "https://cosmos.azure.com",
ManagedHSM: "https://managedhsm.azure.net",
MicrosoftGraph: "https://graph.microsoft.com/",
},
}
@@ -118,26 +152,42 @@ var (
ActiveDirectoryEndpoint: "https://login.microsoftonline.us/",
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
ManagedHSMEndpoint: NotAvailable,
GraphEndpoint: "https://graph.windows.net/",
ServiceBusEndpoint: "https://servicebus.usgovcloudapi.net/",
BatchManagementEndpoint: "https://batch.core.usgovcloudapi.net/",
MicrosoftGraphEndpoint: "https://graph.microsoft.us/",
StorageEndpointSuffix: "core.usgovcloudapi.net",
CosmosDBDNSSuffix: "documents.azure.us",
MariaDBDNSSuffix: "mariadb.database.usgovcloudapi.net",
MySQLDatabaseDNSSuffix: "mysql.database.usgovcloudapi.net",
PostgresqlDatabaseDNSSuffix: "postgres.database.usgovcloudapi.net",
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
KeyVaultDNSSuffix: "vault.usgovcloudapi.net",
ManagedHSMDNSSuffix: NotAvailable,
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
ResourceManagerVMDNSSuffix: "cloudapp.usgovcloudapi.net",
ContainerRegistryDNSSuffix: "azurecr.us",
CosmosDBDNSSuffix: "documents.azure.us",
TokenAudience: "https://management.usgovcloudapi.net/",
APIManagementHostNameSuffix: "azure-api.us",
SynapseEndpointSuffix: "dev.azuresynapse.usgovcloudapi.net",
DatalakeSuffix: NotAvailable,
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.windows.net/",
KeyVault: "https://vault.usgovcloudapi.net",
Datalake: NotAvailable,
Batch: "https://batch.core.usgovcloudapi.net/",
OperationalInsights: "https://api.loganalytics.us",
OSSRDBMS: "https://ossrdbms-aad.database.usgovcloudapi.net",
Storage: "https://storage.azure.com/",
Synapse: "https://dev.azuresynapse.usgovcloudapi.net",
ServiceBus: "https://servicebus.azure.net/",
SQLDatabase: "https://database.usgovcloudapi.net/",
CosmosDB: "https://cosmos.azure.com",
ManagedHSM: NotAvailable,
MicrosoftGraph: "https://graph.microsoft.us/",
},
}
@@ -151,26 +201,42 @@ var (
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn/",
GalleryEndpoint: "https://gallery.chinacloudapi.cn/",
KeyVaultEndpoint: "https://vault.azure.cn/",
ManagedHSMEndpoint: NotAvailable,
GraphEndpoint: "https://graph.chinacloudapi.cn/",
ServiceBusEndpoint: "https://servicebus.chinacloudapi.cn/",
BatchManagementEndpoint: "https://batch.chinacloudapi.cn/",
MicrosoftGraphEndpoint: "https://microsoftgraph.chinacloudapi.cn/",
StorageEndpointSuffix: "core.chinacloudapi.cn",
CosmosDBDNSSuffix: "documents.azure.cn",
MariaDBDNSSuffix: "mariadb.database.chinacloudapi.cn",
MySQLDatabaseDNSSuffix: "mysql.database.chinacloudapi.cn",
PostgresqlDatabaseDNSSuffix: "postgres.database.chinacloudapi.cn",
SQLDatabaseDNSSuffix: "database.chinacloudapi.cn",
TrafficManagerDNSSuffix: "trafficmanager.cn",
KeyVaultDNSSuffix: "vault.azure.cn",
ManagedHSMDNSSuffix: NotAvailable,
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.cn",
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
ResourceManagerVMDNSSuffix: "cloudapp.chinacloudapi.cn",
ContainerRegistryDNSSuffix: "azurecr.cn",
CosmosDBDNSSuffix: "documents.azure.cn",
TokenAudience: "https://management.chinacloudapi.cn/",
APIManagementHostNameSuffix: "azure-api.cn",
SynapseEndpointSuffix: "dev.azuresynapse.azure.cn",
DatalakeSuffix: NotAvailable,
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.chinacloudapi.cn/",
KeyVault: "https://vault.azure.cn",
Datalake: NotAvailable,
Batch: "https://batch.chinacloudapi.cn/",
OperationalInsights: NotAvailable,
OSSRDBMS: "https://ossrdbms-aad.database.chinacloudapi.cn",
Storage: "https://storage.azure.com/",
Synapse: "https://dev.azuresynapse.net",
ServiceBus: "https://servicebus.azure.net/",
SQLDatabase: "https://database.chinacloudapi.cn/",
CosmosDB: "https://cosmos.azure.com",
ManagedHSM: NotAvailable,
MicrosoftGraph: "https://microsoftgraph.chinacloudapi.cn",
},
}
@@ -184,26 +250,42 @@ var (
ActiveDirectoryEndpoint: "https://login.microsoftonline.de/",
GalleryEndpoint: "https://gallery.cloudapi.de/",
KeyVaultEndpoint: "https://vault.microsoftazure.de/",
ManagedHSMEndpoint: NotAvailable,
GraphEndpoint: "https://graph.cloudapi.de/",
ServiceBusEndpoint: "https://servicebus.cloudapi.de/",
BatchManagementEndpoint: "https://batch.cloudapi.de/",
MicrosoftGraphEndpoint: NotAvailable,
StorageEndpointSuffix: "core.cloudapi.de",
CosmosDBDNSSuffix: "documents.microsoftazure.de",
MariaDBDNSSuffix: "mariadb.database.cloudapi.de",
MySQLDatabaseDNSSuffix: "mysql.database.cloudapi.de",
PostgresqlDatabaseDNSSuffix: "postgres.database.cloudapi.de",
SQLDatabaseDNSSuffix: "database.cloudapi.de",
TrafficManagerDNSSuffix: "azuretrafficmanager.de",
KeyVaultDNSSuffix: "vault.microsoftazure.de",
ManagedHSMDNSSuffix: NotAvailable,
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
ContainerRegistryDNSSuffix: NotAvailable,
CosmosDBDNSSuffix: "documents.microsoftazure.de",
TokenAudience: "https://management.microsoftazure.de/",
APIManagementHostNameSuffix: NotAvailable,
SynapseEndpointSuffix: NotAvailable,
DatalakeSuffix: NotAvailable,
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.cloudapi.de/",
KeyVault: "https://vault.microsoftazure.de",
Datalake: NotAvailable,
Batch: "https://batch.cloudapi.de/",
OperationalInsights: NotAvailable,
OSSRDBMS: "https://ossrdbms-aad.database.cloudapi.de",
Storage: "https://storage.azure.com/",
Synapse: NotAvailable,
ServiceBus: "https://servicebus.azure.net/",
SQLDatabase: "https://database.cloudapi.de/",
CosmosDB: "https://cosmos.azure.com",
ManagedHSM: NotAvailable,
MicrosoftGraph: NotAvailable,
},
}
)
@@ -242,3 +324,8 @@ func EnvironmentFromFile(location string) (unmarshaled Environment, err error) {
return
}
// SetEnvironment updates the environment map with the specified values.
func SetEnvironment(name string, env Environment) {
environments[strings.ToUpper(name)] = env
}

View File

@@ -64,7 +64,7 @@ func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator {
if re.ServiceError != nil && re.ServiceError.Code == "MissingSubscriptionRegistration" {
regErr := register(client, r, re)
if regErr != nil {
return resp, fmt.Errorf("failed auto registering Resource Provider: %s. Original error: %s", regErr, err)
return resp, fmt.Errorf("failed auto registering Resource Provider: %s. Original error: %w", regErr, err)
}
}
}

View File

@@ -17,6 +17,7 @@ package autorest
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -30,7 +31,7 @@ import (
const (
// DefaultPollingDelay is a reasonable delay between polling requests.
DefaultPollingDelay = 60 * time.Second
DefaultPollingDelay = 30 * time.Second
// DefaultPollingDuration is a reasonable total polling duration.
DefaultPollingDuration = 15 * time.Minute
@@ -165,7 +166,8 @@ type Client struct {
// Setting this to zero will use the provided context to control the duration.
PollingDuration time.Duration
// RetryAttempts sets the default number of retry attempts for client.
// RetryAttempts sets the total number of times the client will attempt to make an HTTP request.
// Set the value to 1 to disable retries. DO NOT set the value to less than 1.
RetryAttempts int
// RetryDuration sets the delay duration for retries.
@@ -259,6 +261,9 @@ func (c Client) Do(r *http.Request) (*http.Response, error) {
},
})
resp, err := SendWithSender(c.sender(tls.RenegotiateNever), r)
if resp == nil && err == nil {
err = errors.New("autorest: received nil response and error")
}
logger.Instance.WriteResponse(resp, logger.Filter{})
Respond(resp, c.ByInspecting())
return resp, err

View File

@@ -16,9 +16,9 @@ package date
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest/autorest"
import _ "github.com/Azure/go-autorest"

View File

@@ -96,3 +96,8 @@ func (e DetailedError) Error() string {
}
return fmt.Sprintf("%s#%s: %s: StatusCode=%d -- Original Error: %v", e.PackageType, e.Method, e.Message, e.StatusCode, e.Original)
}
// Unwrap returns the original error.
func (e DetailedError) Unwrap() error {
return e.Original
}

View File

@@ -0,0 +1,25 @@
//go:build modhack
// +build modhack
package autorest
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest"

View File

@@ -127,10 +127,7 @@ func WithHeader(header string, value string) PrepareDecorator {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
if r.Header == nil {
r.Header = make(http.Header)
}
r.Header.Set(http.CanonicalHeaderKey(header), value)
setHeader(r, http.CanonicalHeaderKey(header), value)
}
return r, err
})
@@ -230,7 +227,7 @@ func AsPost() PrepareDecorator { return WithMethod("POST") }
func AsPut() PrepareDecorator { return WithMethod("PUT") }
// WithBaseURL returns a PrepareDecorator that populates the http.Request with a url.URL constructed
// from the supplied baseUrl.
// from the supplied baseUrl. Query parameters will be encoded as required.
func WithBaseURL(baseURL string) PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
@@ -241,11 +238,18 @@ func WithBaseURL(baseURL string) PrepareDecorator {
return r, err
}
if u.Scheme == "" {
err = fmt.Errorf("autorest: No scheme detected in URL %s", baseURL)
return r, fmt.Errorf("autorest: No scheme detected in URL %s", baseURL)
}
if err == nil {
r.URL = u
if u.RawQuery != "" {
// handle unencoded semicolons (ideally the server would send them already encoded)
u.RawQuery = strings.Replace(u.RawQuery, ";", "%3B", -1)
q, err := url.ParseQuery(u.RawQuery)
if err != nil {
return r, err
}
u.RawQuery = q.Encode()
}
r.URL = u
}
return r, err
})
@@ -290,10 +294,7 @@ func WithFormData(v url.Values) PrepareDecorator {
if err == nil {
s := v.Encode()
if r.Header == nil {
r.Header = make(http.Header)
}
r.Header.Set(http.CanonicalHeaderKey(headerContentType), mimeTypeFormPost)
setHeader(r, http.CanonicalHeaderKey(headerContentType), mimeTypeFormPost)
r.ContentLength = int64(len(s))
r.Body = ioutil.NopCloser(strings.NewReader(s))
}
@@ -329,10 +330,7 @@ func WithMultiPartFormData(formDataParameters map[string]interface{}) PrepareDec
if err = writer.Close(); err != nil {
return r, err
}
if r.Header == nil {
r.Header = make(http.Header)
}
r.Header.Set(http.CanonicalHeaderKey(headerContentType), writer.FormDataContentType())
setHeader(r, http.CanonicalHeaderKey(headerContentType), writer.FormDataContentType())
r.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
r.ContentLength = int64(body.Len())
return r, err
@@ -437,6 +435,7 @@ func WithXML(v interface{}) PrepareDecorator {
bytesWithHeader := []byte(withHeader)
r.ContentLength = int64(len(bytesWithHeader))
setHeader(r, headerContentLength, fmt.Sprintf("%d", len(bytesWithHeader)))
r.Body = ioutil.NopCloser(bytes.NewReader(bytesWithHeader))
}
}

View File

@@ -1,3 +1,4 @@
//go:build !go1.8
// +build !go1.8
// Copyright 2017 Microsoft Corporation

View File

@@ -1,3 +1,4 @@
//go:build go1.8
// +build go1.8
// Copyright 2017 Microsoft Corporation

View File

@@ -20,14 +20,34 @@ import (
"fmt"
"log"
"math"
"net"
"net/http"
"net/http/cookiejar"
"strconv"
"sync"
"time"
"github.com/Azure/go-autorest/logger"
"github.com/Azure/go-autorest/tracing"
)
// there is one sender per TLS renegotiation type, i.e. count of tls.RenegotiationSupport enums
const defaultSendersCount = 3
type defaultSender struct {
sender Sender
init *sync.Once
}
// each type of sender will be created on demand in sender()
var defaultSenders [defaultSendersCount]defaultSender
func init() {
for i := 0; i < defaultSendersCount; i++ {
defaultSenders[i].init = &sync.Once{}
}
}
// used as a key type in context.WithValue()
type ctxSendDecorators struct{}
@@ -107,26 +127,34 @@ func SendWithSender(s Sender, r *http.Request, decorators ...SendDecorator) (*ht
}
func sender(renengotiation tls.RenegotiationSupport) Sender {
// Use behaviour compatible with DefaultTransport, but require TLS minimum version.
defaultTransport := http.DefaultTransport.(*http.Transport)
transport := &http.Transport{
Proxy: defaultTransport.Proxy,
DialContext: defaultTransport.DialContext,
MaxIdleConns: defaultTransport.MaxIdleConns,
IdleConnTimeout: defaultTransport.IdleConnTimeout,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Renegotiation: renengotiation,
},
}
var roundTripper http.RoundTripper = transport
if tracing.IsEnabled() {
roundTripper = tracing.NewTransport(transport)
}
j, _ := cookiejar.New(nil)
return &http.Client{Jar: j, Transport: roundTripper}
// note that we can't init defaultSenders in init() since it will
// execute before calling code has had a chance to enable tracing
defaultSenders[renengotiation].init.Do(func() {
// copied from http.DefaultTransport with a TLS minimum version.
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Renegotiation: renengotiation,
},
}
var roundTripper http.RoundTripper = transport
if tracing.IsEnabled() {
roundTripper = tracing.NewTransport(transport)
}
j, _ := cookiejar.New(nil)
defaultSenders[renengotiation].sender = &http.Client{Jar: j, Transport: roundTripper}
})
return defaultSenders[renengotiation].sender
}
// AfterDelay returns a SendDecorator that delays for the passed time.Duration before
@@ -248,6 +276,7 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
if err == nil {
return resp, err
}
logger.Instance.Writef(logger.LogError, "DoRetryForAttempts: received error for attempt %d: %v\n", attempt+1, err)
if !DelayForBackoff(backoff, attempt, r.Context().Done()) {
return nil, r.Context().Err()
}
@@ -257,6 +286,12 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
}
}
// Count429AsRetry indicates that a 429 response should be included as a retry attempt.
var Count429AsRetry = true
// Max429Delay is the maximum duration to wait between retries on a 429 if no Retry-After header was received.
var Max429Delay time.Duration
// DoRetryForStatusCodes returns a SendDecorator that retries for specified statusCodes for up to the specified
// number of attempts, exponentially backing off between requests using the supplied backoff
// time.Duration (which may be zero). Retrying may be canceled by cancelling the context on the http.Request.
@@ -264,7 +299,7 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) SendDecorator {
return func(s Sender) Sender {
return SenderFunc(func(r *http.Request) (*http.Response, error) {
return doRetryForStatusCodesImpl(s, r, false, attempts, backoff, 0, codes...)
return doRetryForStatusCodesImpl(s, r, Count429AsRetry, attempts, backoff, 0, codes...)
})
}
}
@@ -276,7 +311,7 @@ func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) Se
func DoRetryForStatusCodesWithCap(attempts int, backoff, cap time.Duration, codes ...int) SendDecorator {
return func(s Sender) Sender {
return SenderFunc(func(r *http.Request) (*http.Response, error) {
return doRetryForStatusCodesImpl(s, r, true, attempts, backoff, cap, codes...)
return doRetryForStatusCodesImpl(s, r, Count429AsRetry, attempts, backoff, cap, codes...)
})
}
}
@@ -296,12 +331,14 @@ func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempt
if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) {
return resp, err
}
if err != nil {
logger.Instance.Writef(logger.LogError, "DoRetryForStatusCodes: received error for attempt %d: %v\n", attempt+1, err)
}
delayed := DelayWithRetryAfter(resp, r.Context().Done())
// enforce a 2 minute cap between requests when 429 status codes are
// not going to be counted as an attempt and when the cap is 0.
// this should only happen in the absence of a retry-after header.
if !count429 && cap == 0 {
cap = 2 * time.Minute
// if this was a 429 set the delay cap as specified.
// applicable only in the absence of a retry-after header.
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
cap = Max429Delay
}
if !delayed && !DelayForBackoffWithCap(backoff, cap, delayCount, r.Context().Done()) {
return resp, r.Context().Err()
@@ -363,6 +400,7 @@ func DoRetryForDuration(d time.Duration, backoff time.Duration) SendDecorator {
if err == nil {
return resp, err
}
logger.Instance.Writef(logger.LogError, "DoRetryForDuration: received error for attempt %d: %v\n", attempt+1, err)
if !DelayForBackoff(backoff, attempt, r.Context().Done()) {
return nil, r.Context().Err()
}
@@ -410,6 +448,7 @@ func DelayForBackoffWithCap(backoff, cap time.Duration, attempt int, cancel <-ch
if cap > 0 && d > cap {
d = cap
}
logger.Instance.Writef(logger.LogInfo, "DelayForBackoffWithCap: sleeping for %s\n", d)
select {
case <-time.After(d):
return true

View File

@@ -26,8 +26,6 @@ import (
"net/url"
"reflect"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
)
// EncodedAs is a series of constants specifying various data encodings
@@ -207,18 +205,6 @@ func ChangeToGet(req *http.Request) *http.Request {
return req
}
// IsTokenRefreshError returns true if the specified error implements the TokenRefreshError
// interface. If err is a DetailedError it will walk the chain of Original errors.
func IsTokenRefreshError(err error) bool {
if _, ok := err.(adal.TokenRefreshError); ok {
return true
}
if de, ok := err.(DetailedError); ok {
return IsTokenRefreshError(de.Original)
}
return false
}
// IsTemporaryNetworkError returns true if the specified error is a temporary network error or false
// if it's not. If the error doesn't implement the net.Error interface the return value is true.
func IsTemporaryNetworkError(err error) bool {
@@ -237,3 +223,10 @@ func DrainResponseBody(resp *http.Response) error {
}
return nil
}
func setHeader(r *http.Request, key, value string) {
if r.Header == nil {
r.Header = make(http.Header)
}
r.Header.Set(key, value)
}

View File

@@ -0,0 +1,30 @@
//go:build go1.13
// +build go1.13
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package autorest
import (
"errors"
"github.com/Azure/go-autorest/autorest/adal"
)
// IsTokenRefreshError returns true if the specified error implements the TokenRefreshError interface.
func IsTokenRefreshError(err error) bool {
var tre adal.TokenRefreshError
return errors.As(err, &tre)
}

View File

@@ -0,0 +1,32 @@
//go:build !go1.13
// +build !go1.13
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package autorest
import "github.com/Azure/go-autorest/autorest/adal"
// IsTokenRefreshError returns true if the specified error implements the TokenRefreshError
// interface. If err is a DetailedError it will walk the chain of Original errors.
func IsTokenRefreshError(err error) bool {
if _, ok := err.(adal.TokenRefreshError); ok {
return true
}
if de, ok := err.(DetailedError); ok {
return IsTokenRefreshError(de.Original)
}
return false
}

View File

@@ -19,7 +19,7 @@ import (
"runtime"
)
const number = "v13.4.0"
const number = "v14.2.1"
var (
userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",

105
vendor/github.com/Azure/go-autorest/azure-pipelines.yml generated vendored Normal file
View File

@@ -0,0 +1,105 @@
variables:
GOPATH: '$(system.defaultWorkingDirectory)/work'
sdkPath: '$(GOPATH)/src/github.com/$(build.repository.name)'
jobs:
- job: 'goautorest'
displayName: 'Run go-autorest CI Checks'
strategy:
matrix:
Linux_Go113:
vm.image: 'ubuntu-18.04'
go.version: '1.13'
Linux_Go114:
vm.image: 'ubuntu-18.04'
go.version: '1.14'
pool:
vmImage: '$(vm.image)'
steps:
- task: GoTool@0
inputs:
version: '$(go.version)'
displayName: "Select Go Version"
- script: |
set -e
mkdir -p '$(GOPATH)/bin'
mkdir -p '$(sdkPath)'
shopt -s extglob
mv !(work) '$(sdkPath)'
echo '##vso[task.prependpath]$(GOPATH)/bin'
displayName: 'Create Go Workspace'
- script: |
set -e
curl -sSL https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure -v
go install ./vendor/golang.org/x/lint/golint
go get github.com/jstemmer/go-junit-report
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
go get -u github.com/matm/gocov-html
workingDirectory: '$(sdkPath)'
displayName: 'Install Dependencies'
- script: |
go vet ./autorest/...
go vet ./logger/...
go vet ./tracing/...
workingDirectory: '$(sdkPath)'
displayName: 'Vet'
- script: |
go build -v ./autorest/...
go build -v ./logger/...
go build -v ./tracing/...
workingDirectory: '$(sdkPath)'
displayName: 'Build'
- script: |
set -e
go test -race -v -coverprofile=coverage.txt -covermode atomic ./autorest/... ./logger/... ./tracing/... 2>&1 | go-junit-report > report.xml
gocov convert coverage.txt > coverage.json
gocov-xml < coverage.json > coverage.xml
gocov-html < coverage.json > coverage.html
workingDirectory: '$(sdkPath)'
displayName: 'Run Tests'
- script: grep -L -r --include *.go --exclude-dir vendor -P "Copyright (\d{4}|\(c\)) Microsoft" ./ | tee >&2
workingDirectory: '$(sdkPath)'
displayName: 'Copyright Header Check'
failOnStderr: true
condition: succeededOrFailed()
- script: |
gofmt -s -l -w ./autorest/. >&2
gofmt -s -l -w ./logger/. >&2
gofmt -s -l -w ./tracing/. >&2
workingDirectory: '$(sdkPath)'
displayName: 'Format Check'
failOnStderr: true
condition: succeededOrFailed()
- script: |
golint ./autorest/... >&2
golint ./logger/... >&2
golint ./tracing/... >&2
workingDirectory: '$(sdkPath)'
displayName: 'Linter Check'
failOnStderr: true
condition: succeededOrFailed()
- task: PublishTestResults@2
inputs:
testRunner: JUnit
testResultsFiles: $(sdkPath)/report.xml
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(sdkPath)/coverage.xml
additionalCodeCoverageFiles: $(sdkPath)/coverage.html

18
vendor/github.com/Azure/go-autorest/doc.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/*
Package go-autorest provides an HTTP request client for use with Autorest-generated API client packages.
*/
package go_autorest
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

View File

@@ -0,0 +1,24 @@
// +build modhack
package logger
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest"

View File

@@ -55,6 +55,10 @@ const (
// LogDebug tells a logger to log all LogDebug, LogInfo, LogWarning, LogError, LogPanic and LogFatal entries passed to it.
LogDebug
// LogAuth is a special case of LogDebug, it tells a logger to also log the body of an authentication request and response.
// NOTE: this can disclose sensitive information, use with care.
LogAuth
)
const (
@@ -65,6 +69,7 @@ const (
logWarning = "WARNING"
logInfo = "INFO"
logDebug = "DEBUG"
logAuth = "AUTH"
logUnknown = "UNKNOWN"
)
@@ -83,6 +88,8 @@ func ParseLevel(s string) (lt LevelType, err error) {
lt = LogInfo
case logDebug:
lt = LogDebug
case logAuth:
lt = LogAuth
default:
err = fmt.Errorf("bad log level '%s'", s)
}
@@ -106,6 +113,8 @@ func (lt LevelType) String() string {
return logInfo
case LogDebug:
return logDebug
case LogAuth:
return logAuth
default:
return logUnknown
}

View File

@@ -0,0 +1,24 @@
// +build modhack
package tracing
// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file, and the github.com/Azure/go-autorest import, won't actually become part of
// the resultant binary.
// Necessary for safely adding multi-module repo.
// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
import _ "github.com/Azure/go-autorest"

View File

@@ -0,0 +1,200 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016-present Datadog, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,85 @@
package obfuscate
import (
"fmt"
"time"
"github.com/dgraph-io/ristretto"
)
// measuredCache is a wrapper on top of *ristretto.Cache which additionally
// sends metrics (hits and misses) every 10 seconds.
type measuredCache struct {
*ristretto.Cache
// close allows sending shutdown notification.
close chan struct{}
statsd StatsClient
}
// Close gracefully closes the cache when active.
func (c *measuredCache) Close() {
if c.Cache == nil {
return
}
c.close <- struct{}{}
<-c.close
}
func (c *measuredCache) statsLoop() {
defer func() {
c.close <- struct{}{}
}()
tick := time.NewTicker(10 * time.Second)
defer tick.Stop()
mx := c.Cache.Metrics
for {
select {
case <-tick.C:
c.statsd.Gauge("datadog.trace_agent.ofuscation.sql_cache.hits", float64(mx.Hits()), nil, 1) //nolint:errcheck
c.statsd.Gauge("datadog.trace_agent.ofuscation.sql_cache.misses", float64(mx.Misses()), nil, 1) //nolint:errcheck
case <-c.close:
c.Cache.Close()
return
}
}
}
type cacheOptions struct {
On bool
Statsd StatsClient
}
// newMeasuredCache returns a new measuredCache.
func newMeasuredCache(opts cacheOptions) *measuredCache {
if !opts.On {
// a nil *ristretto.Cache is a no-op cache
return &measuredCache{}
}
cfg := &ristretto.Config{
// We know that the maximum allowed resource length is 5K. This means that
// in 5MB we can store a minimum of 1000 queries.
MaxCost: 5000000,
// An appromixated worst-case scenario when the cache is filled with small
// queries averaged as being of length 11 ("LOCK TABLES"), we would be able
// to fit 476K of them into 5MB of cost.
//
// We average it to 500K and multiply 10x as the documentation recommends.
NumCounters: 500000 * 10,
BufferItems: 64, // default recommended value
Metrics: true, // enable hit/miss counters
}
cache, err := ristretto.NewCache(cfg)
if err != nil {
panic(fmt.Errorf("Error starting obfuscator query cache: %v", err))
}
c := measuredCache{
close: make(chan struct{}),
statsd: opts.Statsd,
Cache: cache,
}
go c.statsLoop()
return &c
}

View File

@@ -0,0 +1,212 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
// IsCardNumber checks if b could be a credit card number by checking the digit count and IIN prefix.
// If validateLuhn is true, the Luhn checksum is also applied to potential candidates.
func IsCardNumber(b string, validateLuhn bool) (ok bool) {
//
// Just credit card numbers for now, based on:
// • https://baymard.com/checkout-usability/credit-card-patterns
// • https://www.regular-expressions.info/creditcard.html
//
if len(b) == 0 {
return false
}
if len(b) < 12 {
// fast path: can not be a credit card
return false
}
if b[0] != ' ' && b[0] != '-' && (b[0] < '0' || b[0] > '9') {
// fast path: only valid characters are 0-9, space (" ") and dash("-")
return false
}
prefix := 0 // holds up to b[:6] digits as a numeric value (for example []byte{"523"} becomes int(523)) for checking prefixes
count := 0 // counts digits encountered
foundPrefix := false // reports whether we've detected a valid prefix
recdigit := func(_ byte) {} // callback on each found digit; no-op by default (we only need this for Luhn)
if validateLuhn {
// we need Luhn checksum validation, so we have to take additional action
// and record all digits found
buf := make([]byte, 0, len(b))
recdigit = func(b byte) { buf = append(buf, b) }
defer func() {
if !ok {
// if isCardNumber returned false, it means that b can not be
// a credit card number
return
}
// potentially a credit card number, run the Luhn checksum
ok = luhnValid(buf)
}()
}
loop:
for i := range b {
// We traverse and search b for a valid IIN credit card prefix based
// on the digits found, ignoring spaces and dashes.
// Source: https://www.regular-expressions.info/creditcard.html
switch b[i] {
case ' ', '-':
// ignore space (' ') and dash ('-')
continue loop
}
if b[i] < '0' || b[i] > '9' {
// not a 0 to 9 digit; can not be a credit card number; abort
return false
}
count++
recdigit(b[i])
if !foundPrefix {
// we have not yet found a valid prefix so we convert the digits
// that we have so far into a numeric value:
prefix = prefix*10 + (int(b[i]) - '0')
maybe, yes := validCardPrefix(prefix)
if yes {
// we've found a valid prefix; continue counting
foundPrefix = true
} else if !maybe {
// this is not a valid prefix and we should not continue looking
return false
}
}
if count > 16 {
// too many digits
return false
}
}
if count < 12 {
// too few digits
return false
}
return foundPrefix
}
// luhnValid checks that the number represented in the given string validates the Luhn Checksum algorithm.
// str is expected to contain exclusively digits at all positions.
//
// See:
// • https://en.wikipedia.org/wiki/Luhn_algorithm
// • https://dev.to/shiraazm/goluhn-a-simple-library-for-generating-calculating-and-verifying-luhn-numbers-588j
//
func luhnValid(str []byte) bool {
var (
sum int
alt bool
)
n := len(str)
for i := n - 1; i > -1; i-- {
if str[i] < '0' || str[i] > '9' {
return false // not a number!
}
mod := int(str[i] - 0x30) // convert byte to int
if alt {
mod *= 2
if mod > 9 {
mod = (mod % 10) + 1
}
}
alt = !alt
sum += mod
}
return sum%10 == 0
}
// validCardPrefix validates whether b is a valid card prefix. Maybe returns true if
// the prefix could be an IIN once more digits are revealed and yes reports whether
// b is a fully valid IIN.
//
// If yes is false and maybe is false, there is no reason to continue searching. The
// prefix is invalid.
//
// IMPORTANT: If adding new prefixes to this algorithm, make sure that you update
// the "maybe" clauses above, in the shorter prefixes than the one you are adding.
// This refers to the cases which return true, false.
//
// TODO(x): this whole code could be code generated from a prettier data structure.
// Ultimately, it could even be user-configurable.
func validCardPrefix(n int) (maybe, yes bool) {
// Validates IIN prefix possibilities
// Source: https://www.regular-expressions.info/creditcard.html
if n > 699999 {
// too long for any known prefix; stop looking
return false, false
}
if n < 10 {
switch n {
case 1, 4:
// 1 & 4 are valid IIN
return false, true
case 2, 3, 5, 6:
// 2, 3, 5, 6 could be the start of valid IIN
return true, false
default:
// invalid IIN
return false, false
}
}
if n < 100 {
if (n >= 34 && n <= 39) ||
(n >= 51 && n <= 55) ||
n == 62 ||
n == 65 {
// 34-39, 51-55, 62, 65 are valid IIN
return false, true
}
if n == 30 || n == 63 || n == 64 || n == 35 || n == 50 || n == 60 ||
(n >= 22 && n <= 27) || (n >= 56 && n <= 58) || (n >= 60 && n <= 69) {
// 30, 63, 64, 35, 50, 60, 22-27, 56-58, 60-69 may end up as valid IIN
return true, false
}
}
if n < 1000 {
if (n >= 300 && n <= 305) ||
(n >= 644 && n <= 649) ||
n == 309 ||
n == 636 {
// 300305, 309, 636, 644649 are valid IIN
return false, true
}
if (n >= 352 && n <= 358) || n == 501 || n == 601 ||
(n >= 222 && n <= 272) || (n >= 500 && n <= 509) ||
(n >= 560 && n <= 589) || (n >= 600 && n <= 699) {
// 352-358, 501, 601, 222-272, 500-509, 560-589, 600-699 may be a 4 or 6 digit IIN prefix
return true, false
}
}
if n < 10000 {
if (n >= 3528 && n <= 3589) ||
n == 5019 ||
n == 6011 {
// 35283589, 5019, 6011 are valid IINs
return false, true
}
if (n >= 2221 && n <= 2720) || (n >= 5000 && n <= 5099) ||
(n >= 5600 && n <= 5899) || (n >= 6000 && n <= 6999) {
// maybe a 6-digit IIN
return true, false
}
}
if n < 100000 {
if (n >= 22210 && n <= 27209) ||
(n >= 50000 && n <= 50999) ||
(n >= 56000 && n <= 58999) ||
(n >= 60000 && n <= 69999) {
// maybe a 6-digit IIN
return true, false
}
}
if n < 1000000 {
if (n >= 222100 && n <= 272099) ||
(n >= 500000 && n <= 509999) ||
(n >= 560000 && n <= 589999) ||
(n >= 600000 && n <= 699999) {
// 222100272099, 500000509999, 560000589999, 600000699999 are valid IIN
return false, true
}
}
// unknown IIN
return false, false
}

View File

@@ -0,0 +1,50 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"net/url"
"strings"
)
// ObfuscateURLString obfuscates the given URL. It must be a valid URL and at least one
// HTTP obfuscation option must be enabled at Obfuscator instantiation time.
func (o *Obfuscator) ObfuscateURLString(val string) string {
if !o.opts.HTTP.RemoveQueryString && !o.opts.HTTP.RemovePathDigits {
// nothing to do
return val
}
u, err := url.Parse(val)
if err != nil {
// should not happen for valid URLs, but better obfuscate everything
// rather than expose sensitive information when this option is on.
return "?"
}
if o.opts.HTTP.RemoveQueryString && u.RawQuery != "" {
u.ForceQuery = true // add the '?'
u.RawQuery = ""
}
if o.opts.HTTP.RemovePathDigits {
segs := strings.Split(u.Path, "/")
var changed bool
for i, seg := range segs {
for _, ch := range []byte(seg) {
if ch >= '0' && ch <= '9' {
// we can not set the question mark directly here because the url
// package will escape it into %3F, so we use this placeholder and
// replace it further down.
segs[i] = "/REDACTED/"
changed = true
break
}
}
}
if changed {
u.Path = strings.Join(segs, "/")
}
}
return strings.Replace(u.String(), "/REDACTED/", "?", -1)
}

View File

@@ -0,0 +1,201 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"strconv"
"strings"
)
// ObfuscateMongoDBString obfuscates the given MongoDB JSON query.
func (o *Obfuscator) ObfuscateMongoDBString(cmd string) string {
return obfuscateJSONString(cmd, o.mongo)
}
// ObfuscateElasticSearchString obfuscates the given ElasticSearch JSON query.
func (o *Obfuscator) ObfuscateElasticSearchString(cmd string) string {
return obfuscateJSONString(cmd, o.es)
}
// obfuscateJSONString obfuscates the given span's tag using the given obfuscator. If the obfuscator is
// nil it is considered disabled.
func obfuscateJSONString(cmd string, obfuscator *jsonObfuscator) string {
if obfuscator == nil || cmd == "" {
// obfuscator is disabled or string is empty
return cmd
}
out, _ := obfuscator.obfuscate([]byte(cmd))
// we should accept whatever the obfuscator returns, even if it's an error: a parsing
// error simply means that the JSON was invalid, meaning that we've only obfuscated
// as much of it as we could. It is safe to accept the output, even if partial.
return out
}
type jsonObfuscator struct {
keepKeys map[string]bool // the values for these keys will not be obfuscated
transformKeys map[string]bool // the values for these keys pass through the transformer
transformer func(string) string
scan *scanner // scanner
closures []bool // closure stack, true if object (e.g. {[{ => []bool{true, false, true})
key bool // true if scanning a key
wiped bool // true if obfuscation string (`"?"`) was already written for current value
keeping bool // true if not obfuscating
transformingValue bool // true if collecting the next literal for transformation
keepDepth int // the depth at which we've stopped obfuscating
}
func newJSONObfuscator(cfg *JSONConfig, o *Obfuscator) *jsonObfuscator {
keepValue := make(map[string]bool, len(cfg.KeepValues))
for _, v := range cfg.KeepValues {
keepValue[v] = true
}
var (
transformKeys map[string]bool
transformer func(string) string
)
if len(cfg.ObfuscateSQLValues) > 0 {
transformer = sqlObfuscationTransformer(o)
transformKeys = make(map[string]bool, len(cfg.ObfuscateSQLValues))
for _, v := range cfg.ObfuscateSQLValues {
transformKeys[v] = true
}
}
return &jsonObfuscator{
closures: []bool{},
keepKeys: keepValue,
transformKeys: transformKeys,
transformer: transformer,
scan: &scanner{},
}
}
func sqlObfuscationTransformer(o *Obfuscator) func(string) string {
return func(s string) string {
result, err := o.ObfuscateSQLString(s)
if err != nil {
o.log.Debugf("Failed to obfuscate SQL string '%s': %s", s, err.Error())
// instead of returning an empty string we explicitly return an error string here within the result in order
// to surface the problem clearly to the user
return "Datadog-agent failed to obfuscate SQL string. Enable agent debug logs for more info."
}
return result.Query
}
}
// setKey verifies if we are currently scanning a key based on the current state
// and updates the state accordingly. It must be called only after a closure or a
// value scan has ended.
func (p *jsonObfuscator) setKey() {
n := len(p.closures)
p.key = n == 0 || p.closures[n-1] // true if we are at top level or in an object
p.wiped = false
}
func (p *jsonObfuscator) obfuscate(data []byte) (string, error) {
var out strings.Builder
keyBuf := make([]byte, 0, 10) // recording key token
valBuf := make([]byte, 0, 10) // recording value
p.scan.reset()
for _, c := range data {
p.scan.bytes++
op := p.scan.step(p.scan, c)
depth := len(p.closures)
switch op {
case scanBeginObject:
// object begins: {
p.closures = append(p.closures, true)
p.setKey()
p.transformingValue = false
case scanBeginArray:
// array begins: [
p.closures = append(p.closures, false)
p.setKey()
p.transformingValue = false
case scanEndArray, scanEndObject:
// array or object closing
if n := len(p.closures) - 1; n > 0 {
p.closures = p.closures[:n]
}
fallthrough
case scanObjectValue, scanArrayValue:
// done scanning value
p.setKey()
if p.transformingValue && p.transformer != nil {
v, err := strconv.Unquote(string(valBuf))
if err != nil {
v = string(valBuf)
}
result := p.transformer(v)
out.WriteByte('"')
out.WriteString(result)
out.WriteByte('"')
p.transformingValue = false
valBuf = valBuf[:0]
} else if p.keeping && depth < p.keepDepth {
p.keeping = false
}
case scanBeginLiteral, scanContinue:
// starting or continuing a literal
if p.transformingValue {
valBuf = append(valBuf, c)
continue
} else if p.key {
// it's a key
keyBuf = append(keyBuf, c)
} else if !p.keeping {
// it's a value we're not keeping
if !p.wiped {
out.Write([]byte(`"?"`))
p.wiped = true
}
continue
}
case scanObjectKey:
// done scanning key
k := strings.Trim(string(keyBuf), `"`)
if !p.keeping && p.keepKeys[k] {
// we should not obfuscate values of this key
p.keeping = true
p.keepDepth = depth + 1
} else if !p.transformingValue && p.transformer != nil && p.transformKeys[k] {
// the string value immediately following this key will be passed through the value transformer
// if anything other than a literal is found then sql obfuscation is stopped and json obfuscation
// proceeds as usual
p.transformingValue = true
}
keyBuf = keyBuf[:0]
p.key = false
case scanSkipSpace:
continue
case scanError:
// we've encountered an error, mark that there might be more JSON
// using the ellipsis and return whatever we've managed to obfuscate
// thus far.
out.Write([]byte("..."))
return out.String(), p.scan.err
}
out.WriteByte(c)
}
if p.scan.eof() == scanError {
// if an error occurred it's fine, simply add the ellipsis to indicate
// that the input has been truncated.
out.Write([]byte("..."))
return out.String(), p.scan.err
}
return out.String(), nil
}

View File

@@ -0,0 +1,565 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// The code that follows is copied from go/src/encoding/json/scanner.go
// It may contain minor edits, such as allowing multiple JSON objects within
// the same input string (see stateEndTop)
//
package obfuscate
import "strconv"
// A SyntaxError is a description of a JSON syntax error.
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
// A scanner is a JSON scanning state machine.
// Callers call scan.reset() and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in. (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// 1-byte redo (see undo method)
redo bool
// total bytes consumed, updated by decoder.Decode
bytes int64
}
// These values are returned by the state transition functions
// assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that
// callers might be interested to know about.
// It is okay to ignore the return value of any particular
// call to scanner.state: if one call returns scanError,
// every subsequent call will return scanError too.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
// These values are stored in the parseState stack.
// They give the current state of a composite value
// being scanned. If the parser is inside a nested value
// the parseState describes the nested state, outermost at entry 0.
const (
parseObjectKey = iota // parsing object key (before colon)
parseObjectValue // parsing object value (after colon)
parseArrayValue // parsing array value
)
// reset prepares the scanner for use.
// It must be called before calling s.step.
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.redo = false
s.endTop = false
}
// eof tells the scanner that the end of input has been reached.
// It returns a scan status just as s.step does.
func (s *scanner) eof() int {
if s.err != nil {
return scanError
}
if s.endTop {
return scanEnd
}
s.step(s, ' ')
if s.endTop {
return scanEnd
}
if s.err == nil {
s.err = &SyntaxError{"unexpected end of JSON input", s.bytes}
}
return scanError
}
// pushParseState pushes a new parse state p onto the parse stack.
func (s *scanner) pushParseState(p int) {
s.parseState = append(s.parseState, p)
}
// popParseState pops a parse state (already obtained) off the stack
// and updates s.step accordingly.
func (s *scanner) popParseState() {
n := len(s.parseState) - 1
s.parseState = s.parseState[0:n]
s.redo = false
if n == 0 {
s.step = stateEndTop
s.endTop = true
} else {
s.step = stateEndValue
}
}
func isSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
// stateBeginValueOrEmpty is the state after reading `[`.
func stateBeginValueOrEmpty(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == ']' {
return stateEndValue(s, c)
}
return stateBeginValue(s, c)
}
// stateBeginValue is the state at the beginning of the input.
func stateBeginValue(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
switch c {
case '{':
s.step = stateBeginStringOrEmpty
s.pushParseState(parseObjectKey)
return scanBeginObject
case '[':
s.step = stateBeginValueOrEmpty
s.pushParseState(parseArrayValue)
return scanBeginArray
case '"':
s.step = stateInString
return scanBeginLiteral
case '-':
s.step = stateNeg
return scanBeginLiteral
case '0': // beginning of 0.123
s.step = state0
return scanBeginLiteral
case 't': // beginning of true
s.step = stateT
return scanBeginLiteral
case 'f': // beginning of false
s.step = stateF
return scanBeginLiteral
case 'n': // beginning of null
s.step = stateN
return scanBeginLiteral
}
if '1' <= c && c <= '9' { // beginning of 1234.5
s.step = state1
return scanBeginLiteral
}
return s.error(c, "looking for beginning of value")
}
// stateBeginStringOrEmpty is the state after reading `{`.
func stateBeginStringOrEmpty(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == '}' {
n := len(s.parseState)
s.parseState[n-1] = parseObjectValue
return stateEndValue(s, c)
}
return stateBeginString(s, c)
}
// stateBeginString is the state after reading `{"key": value,`.
func stateBeginString(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == '"' {
s.step = stateInString
return scanBeginLiteral
}
return s.error(c, "looking for beginning of object key string")
}
// stateEndValue is the state after completing a value,
// such as after reading `{}` or `true` or `["x"`.
func stateEndValue(s *scanner, c byte) int {
n := len(s.parseState)
if n == 0 {
// Completed top-level before the current byte.
s.step = stateEndTop
s.endTop = true
return stateEndTop(s, c)
}
if c <= ' ' && isSpace(c) {
s.step = stateEndValue
return scanSkipSpace
}
ps := s.parseState[n-1]
switch ps {
case parseObjectKey:
if c == ':' {
s.parseState[n-1] = parseObjectValue
s.step = stateBeginValue
return scanObjectKey
}
return s.error(c, "after object key")
case parseObjectValue:
if c == ',' {
s.parseState[n-1] = parseObjectKey
s.step = stateBeginString
return scanObjectValue
}
if c == '}' {
s.popParseState()
return scanEndObject
}
return s.error(c, "after object key:value pair")
case parseArrayValue:
if c == ',' {
s.step = stateBeginValue
return scanArrayValue
}
if c == ']' {
s.popParseState()
return scanEndArray
}
return s.error(c, "after array element")
}
return s.error(c, "")
}
// stateEndTop is the state after finishing the top-level value,
// such as after reading `{}` or `[1,2,3]`.
// Only space characters should be seen now.
func stateEndTop(s *scanner, c byte) int {
if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
// The former behaviour has been removed. Now, if anything
// other than whitespace follows, we assume a new JSON string
// might be starting. This allows us to continue obfuscating
// further strings in cases where there are multiple JSON
// objects enumerated sequentially within the same input.
// This is a common case for ElasticSearch response bodies.
s.reset()
return s.step(s, c)
}
return scanEnd
}
// stateInString is the state after reading `"`.
func stateInString(s *scanner, c byte) int {
if c == '"' {
s.step = stateEndValue
return scanContinue
}
if c == '\\' {
s.step = stateInStringEsc
return scanContinue
}
if c < 0x20 {
return s.error(c, "in string literal")
}
return scanContinue
}
// stateInStringEsc is the state after reading `"\` during a quoted string.
func stateInStringEsc(s *scanner, c byte) int {
switch c {
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
s.step = stateInString
return scanContinue
case 'u':
s.step = stateInStringEscU
return scanContinue
}
return s.error(c, "in string escape code")
}
// stateInStringEscU is the state after reading `"\u` during a quoted string.
func stateInStringEscU(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU1
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU1 is the state after reading `"\u1` during a quoted string.
func stateInStringEscU1(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU12
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU12 is the state after reading `"\u12` during a quoted string.
func stateInStringEscU12(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU123
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU123 is the state after reading `"\u123` during a quoted string.
func stateInStringEscU123(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInString
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateNeg is the state after reading `-` during a number.
func stateNeg(s *scanner, c byte) int {
if c == '0' {
s.step = state0
return scanContinue
}
if '1' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return s.error(c, "in numeric literal")
}
// state1 is the state after reading a non-zero integer during a number,
// such as after reading `1` or `100` but not `0`.
func state1(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return state0(s, c)
}
// state0 is the state after reading `0` during a number.
func state0(s *scanner, c byte) int {
if c == '.' {
s.step = stateDot
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateDot is the state after reading the integer and decimal point in a number,
// such as after reading `1.`.
func stateDot(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateDot0
return scanContinue
}
return s.error(c, "after decimal point in numeric literal")
}
// stateDot0 is the state after reading the integer, decimal point, and subsequent
// digits of a number, such as after reading `3.14`.
func stateDot0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateE is the state after reading the mantissa and e in a number,
// such as after reading `314e` or `0.314e`.
func stateE(s *scanner, c byte) int {
if c == '+' || c == '-' {
s.step = stateESign
return scanContinue
}
return stateESign(s, c)
}
// stateESign is the state after reading the mantissa, e, and sign in a number,
// such as after reading `314e-` or `0.314e+`.
func stateESign(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateE0
return scanContinue
}
return s.error(c, "in exponent of numeric literal")
}
// stateE0 is the state after reading the mantissa, e, optional sign,
// and at least one digit of the exponent in a number,
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
func stateE0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
return stateEndValue(s, c)
}
// stateT is the state after reading `t`.
func stateT(s *scanner, c byte) int {
if c == 'r' {
s.step = stateTr
return scanContinue
}
return s.error(c, "in literal true (expecting 'r')")
}
// stateTr is the state after reading `tr`.
func stateTr(s *scanner, c byte) int {
if c == 'u' {
s.step = stateTru
return scanContinue
}
return s.error(c, "in literal true (expecting 'u')")
}
// stateTru is the state after reading `tru`.
func stateTru(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal true (expecting 'e')")
}
// stateF is the state after reading `f`.
func stateF(s *scanner, c byte) int {
if c == 'a' {
s.step = stateFa
return scanContinue
}
return s.error(c, "in literal false (expecting 'a')")
}
// stateFa is the state after reading `fa`.
func stateFa(s *scanner, c byte) int {
if c == 'l' {
s.step = stateFal
return scanContinue
}
return s.error(c, "in literal false (expecting 'l')")
}
// stateFal is the state after reading `fal`.
func stateFal(s *scanner, c byte) int {
if c == 's' {
s.step = stateFals
return scanContinue
}
return s.error(c, "in literal false (expecting 's')")
}
// stateFals is the state after reading `fals`.
func stateFals(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal false (expecting 'e')")
}
// stateN is the state after reading `n`.
func stateN(s *scanner, c byte) int {
if c == 'u' {
s.step = stateNu
return scanContinue
}
return s.error(c, "in literal null (expecting 'u')")
}
// stateNu is the state after reading `nu`.
func stateNu(s *scanner, c byte) int {
if c == 'l' {
s.step = stateNul
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateNul is the state after reading `nul`.
func stateNul(s *scanner, c byte) int {
if c == 'l' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateError is the state after reaching a syntax error,
// such as after reading `[1}` or `5.1.2`.
func stateError(s *scanner, c byte) int {
return scanError
}
// error records an error and switches to the error state.
func (s *scanner) error(c byte, context string) int {
s.step = stateError
s.err = &SyntaxError{"invalid character " + quoteChar(c) + " " + context, s.bytes}
return scanError
}
// quoteChar formats c as a quoted character literal
func quoteChar(c byte) string {
// special cases - different from quoted strings
if c == '\'' {
return `'\''`
}
if c == '"' {
return `'"'`
}
// use quoted string with different quotation marks
s := strconv.Quote(string(c))
return "'" + s[1:len(s)-1] + "'"
}

View File

@@ -0,0 +1,19 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import "strings"
// ObfuscateMemcachedString obfuscates the Memcached command cmd.
func (*Obfuscator) ObfuscateMemcachedString(cmd string) string {
// All memcached commands end with new lines [1]. In the case of storage
// commands, key values follow after. Knowing this, all we have to do
// to obfuscate sensitive information is to remove everything that follows
// a new line. For non-storage commands, this will have no effect.
// [1]: https://github.com/memcached/memcached/blob/master/doc/protocol.txt
out := strings.SplitN(cmd, "\r\n", 2)[0]
return strings.TrimSpace(out)
}

View File

@@ -0,0 +1,233 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
// Package obfuscate implements quantizing and obfuscating of tags and resources for
// a set of spans matching a certain criteria.
//
// This module is used in the Datadog Agent, the Go tracing client (dd-trace-go) and in the
// OpenTelemetry Collector Datadog exporter./ End-user behavior is stable, but there are no
// stability guarantees on its public Go API. Nonetheless, if editing try to avoid breaking
// API changes if possible and double check the API usage on all module dependents.
package obfuscate
import (
"bytes"
"sync/atomic"
"github.com/DataDog/datadog-go/statsd"
)
//go:generate easyjson -no_std_marshalers $GOFILE
// Obfuscator quantizes and obfuscates spans. The obfuscator is not safe for
// concurrent use.
type Obfuscator struct {
opts *Config
es *jsonObfuscator // nil if disabled
mongo *jsonObfuscator // nil if disabled
sqlExecPlan *jsonObfuscator // nil if disabled
sqlExecPlanNormalize *jsonObfuscator // nil if disabled
// sqlLiteralEscapes reports whether we should treat escape characters literally or as escape characters.
// A non-zero value means 'yes'. Different SQL engines behave in different ways and the tokenizer needs
// to be generic.
// Not safe for concurrent use.
sqlLiteralEscapes int32
// queryCache keeps a cache of already obfuscated queries.
queryCache *measuredCache
log Logger
}
// Logger is able to log certain log messages.
type Logger interface {
// Debugf logs the given message using the given format.
Debugf(format string, params ...interface{})
}
type noopLogger struct{}
func (noopLogger) Debugf(_ string, _ ...interface{}) {}
// setSQLLiteralEscapes sets whether or not escape characters should be treated literally by the SQL obfuscator.
func (o *Obfuscator) setSQLLiteralEscapes(ok bool) {
if ok {
atomic.StoreInt32(&o.sqlLiteralEscapes, 1)
} else {
atomic.StoreInt32(&o.sqlLiteralEscapes, 0)
}
}
// useSQLLiteralEscapes reports whether escape characters will be treated literally by the SQL obfuscator.
// Some SQL engines require it and others don't. It will be detected as SQL queries are being obfuscated
// through calls to ObfuscateSQLString and automatically set for future.
func (o *Obfuscator) useSQLLiteralEscapes() bool {
return atomic.LoadInt32(&o.sqlLiteralEscapes) == 1
}
// Config holds the configuration for obfuscating sensitive data for various span types.
type Config struct {
// SQL holds the obfuscation configuration for SQL queries.
SQL SQLConfig
// ES holds the obfuscation configuration for ElasticSearch bodies.
ES JSONConfig
// Mongo holds the obfuscation configuration for MongoDB queries.
Mongo JSONConfig
// SQLExecPlan holds the obfuscation configuration for SQL Exec Plans. This is strictly for safety related obfuscation,
// not normalization. Normalization of exec plans is configured in SQLExecPlanNormalize.
SQLExecPlan JSONConfig
// SQLExecPlanNormalize holds the normalization configuration for SQL Exec Plans.
SQLExecPlanNormalize JSONConfig
// HTTP holds the obfuscation settings for HTTP URLs.
HTTP HTTPConfig
// Statsd specifies the statsd client to use for reporting metrics.
Statsd StatsClient
// Logger specifies the logger to use when outputting messages.
// If unset, no logs will be outputted.
Logger Logger
}
// StatsClient implementations are able to emit stats.
type StatsClient interface {
// Gauge reports a gauge stat with the given name, value, tags and rate.
Gauge(name string, value float64, tags []string, rate float64) error
}
// SQLConfig holds the config for obfuscating SQL.
// easyjson:json
type SQLConfig struct {
// TableNames specifies whether the obfuscator should also extract the table names that a query addresses,
// in addition to obfuscating.
TableNames bool
// ReplaceDigits specifies whether digits in table names and identifiers should be obfuscated.
ReplaceDigits bool `json:"replace_digits"`
// KeepSQLAlias reports whether SQL aliases ("AS") should be truncated.
KeepSQLAlias bool
// DollarQuotedFunc reports whether to treat "$func$" delimited dollar-quoted strings
// differently and not obfuscate them as a string. To read more about dollar quoted
// strings see:
//
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
DollarQuotedFunc bool
// Cache reports whether the obfuscator should use a LRU look-up cache for SQL obfuscations.
Cache bool
}
// HTTPConfig holds the configuration settings for HTTP obfuscation.
type HTTPConfig struct {
// RemoveQueryStrings determines query strings to be removed from HTTP URLs.
RemoveQueryString bool
// RemovePathDigits determines digits in path segments to be obfuscated.
RemovePathDigits bool
}
// JSONConfig holds the obfuscation configuration for sensitive
// data found in JSON objects.
type JSONConfig struct {
// Enabled will specify whether obfuscation should be enabled.
Enabled bool
// KeepValues will specify a set of keys for which their values will
// not be obfuscated.
KeepValues []string
// ObfuscateSQLValues will specify a set of keys for which their values
// will be passed through SQL obfuscation
ObfuscateSQLValues []string
}
// NewObfuscator creates a new obfuscator
func NewObfuscator(cfg Config) *Obfuscator {
if cfg.Logger == nil {
cfg.Logger = noopLogger{}
}
o := Obfuscator{
opts: &cfg,
queryCache: newMeasuredCache(cacheOptions{On: cfg.SQL.Cache, Statsd: cfg.Statsd}),
}
if cfg.ES.Enabled {
o.es = newJSONObfuscator(&cfg.ES, &o)
}
if cfg.Mongo.Enabled {
o.mongo = newJSONObfuscator(&cfg.Mongo, &o)
}
if cfg.SQLExecPlan.Enabled {
o.sqlExecPlan = newJSONObfuscator(&cfg.SQLExecPlan, &o)
}
if cfg.SQLExecPlanNormalize.Enabled {
o.sqlExecPlanNormalize = newJSONObfuscator(&cfg.SQLExecPlanNormalize, &o)
}
if cfg.Statsd == nil {
cfg.Statsd = &statsd.NoOpClient{}
}
return &o
}
// Stop cleans up after a finished Obfuscator.
func (o *Obfuscator) Stop() {
o.queryCache.Close()
}
// compactWhitespaces compacts all whitespaces in t.
func compactWhitespaces(t string) string {
n := len(t)
r := make([]byte, n)
spaceCode := uint8(32)
isWhitespace := func(char uint8) bool { return char == spaceCode }
nr := 0
offset := 0
for i := 0; i < n; i++ {
if isWhitespace(t[i]) {
copy(r[nr:], t[nr+offset:i])
r[i-offset] = spaceCode
nr = i + 1 - offset
for j := i + 1; j < n; j++ {
if !isWhitespace(t[j]) {
offset += j - i - 1
i = j
break
} else if j == n-1 {
offset += j - i
i = j
break
}
}
}
}
copy(r[nr:], t[nr+offset:n])
r = r[:n-offset]
return string(bytes.Trim(r, " "))
}
// replaceDigits replaces consecutive sequences of digits with '?',
// example: "jobs_2020_1597876964" --> "jobs_?_?"
func replaceDigits(buffer []byte) []byte {
scanningDigit := false
filtered := buffer[:0]
for _, b := range buffer {
// digits are encoded as 1 byte in utf8
if isDigit(rune(b)) {
if scanningDigit {
continue
}
scanningDigit = true
filtered = append(filtered, byte('?'))
continue
}
scanningDigit = false
filtered = append(filtered, b)
}
return filtered
}

View File

@@ -0,0 +1,100 @@
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package obfuscate
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjson4ef41860DecodeGithubComDataDogDatadogAgentPkgObfuscate(in *jlexer.Lexer, out *SQLConfig) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "TableNames":
out.TableNames = bool(in.Bool())
case "replace_digits":
out.ReplaceDigits = bool(in.Bool())
case "KeepSQLAlias":
out.KeepSQLAlias = bool(in.Bool())
case "DollarQuotedFunc":
out.DollarQuotedFunc = bool(in.Bool())
case "Cache":
out.Cache = bool(in.Bool())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson4ef41860EncodeGithubComDataDogDatadogAgentPkgObfuscate(out *jwriter.Writer, in SQLConfig) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"TableNames\":"
out.RawString(prefix[1:])
out.Bool(bool(in.TableNames))
}
{
const prefix string = ",\"replace_digits\":"
out.RawString(prefix)
out.Bool(bool(in.ReplaceDigits))
}
{
const prefix string = ",\"KeepSQLAlias\":"
out.RawString(prefix)
out.Bool(bool(in.KeepSQLAlias))
}
{
const prefix string = ",\"DollarQuotedFunc\":"
out.RawString(prefix)
out.Bool(bool(in.DollarQuotedFunc))
}
{
const prefix string = ",\"Cache\":"
out.RawString(prefix)
out.Bool(bool(in.Cache))
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v SQLConfig) MarshalEasyJSON(w *jwriter.Writer) {
easyjson4ef41860EncodeGithubComDataDogDatadogAgentPkgObfuscate(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *SQLConfig) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson4ef41860DecodeGithubComDataDogDatadogAgentPkgObfuscate(l, v)
}

View File

@@ -0,0 +1,262 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"strings"
)
// redisTruncationMark is used as suffix by tracing libraries to indicate that a
// command was truncated.
const redisTruncationMark = "..."
const maxRedisNbCommands = 3
// Redis commands consisting in 2 words
var redisCompoundCommandSet = map[string]bool{
"CLIENT": true, "CLUSTER": true, "COMMAND": true, "CONFIG": true, "DEBUG": true, "SCRIPT": true}
// QuantizeRedisString returns a quantized version of a Redis query.
//
// TODO(gbbr): Refactor this method to use the tokenizer and
// remove "compactWhitespaces". This method is buggy when commands
// contain quoted strings with newlines.
func (*Obfuscator) QuantizeRedisString(query string) string {
query = compactWhitespaces(query)
var resource strings.Builder
truncated := false
nbCmds := 0
for len(query) > 0 && nbCmds < maxRedisNbCommands {
var rawLine string
// Read the next command
idx := strings.IndexByte(query, '\n')
if idx == -1 {
rawLine = query
query = ""
} else {
rawLine = query[:idx]
query = query[idx+1:]
}
line := strings.Trim(rawLine, " ")
if len(line) == 0 {
continue
}
// Parse arguments
args := strings.SplitN(line, " ", 3)
if strings.HasSuffix(args[0], redisTruncationMark) {
truncated = true
continue
}
command := strings.ToUpper(args[0])
if redisCompoundCommandSet[command] && len(args) > 1 {
if strings.HasSuffix(args[1], redisTruncationMark) {
truncated = true
continue
}
command += " " + strings.ToUpper(args[1])
}
// Write the command representation
resource.WriteByte(' ')
resource.WriteString(command)
nbCmds++
truncated = false
}
if nbCmds == maxRedisNbCommands || truncated {
resource.WriteString(" ...")
}
return strings.Trim(resource.String(), " ")
}
// ObfuscateRedisString obfuscates the given Redis command.
func (*Obfuscator) ObfuscateRedisString(rediscmd string) string {
t := newRedisTokenizer([]byte(rediscmd))
var (
str strings.Builder
cmd string
args []string
)
for {
tok, typ, done := t.scan()
switch typ {
case redisTokenCommand:
// new command starting
if cmd != "" {
// a previous command was buffered, obfuscate it
obfuscateRedisCmd(&str, cmd, args...)
str.WriteByte('\n')
}
cmd = tok
args = args[:0]
case redisTokenArgument:
args = append(args, tok)
}
if done {
// last command
obfuscateRedisCmd(&str, cmd, args...)
break
}
}
return str.String()
}
func obfuscateRedisCmd(out *strings.Builder, cmd string, args ...string) {
out.WriteString(cmd)
if len(args) == 0 {
return
}
out.WriteByte(' ')
switch strings.ToUpper(cmd) {
case "AUTH":
// Obfuscate everything after command
// • AUTH password
if len(args) > 0 {
args[0] = "?"
args = args[:1]
}
case "APPEND", "GETSET", "LPUSHX", "GEORADIUSBYMEMBER", "RPUSHX",
"SET", "SETNX", "SISMEMBER", "ZRANK", "ZREVRANK", "ZSCORE":
// Obfuscate 2nd argument:
// • APPEND key value
// • GETSET key value
// • LPUSHX key value
// • GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
// • RPUSHX key value
// • SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
// • SETNX key value
// • SISMEMBER key member
// • ZRANK key member
// • ZREVRANK key member
// • ZSCORE key member
obfuscateRedisArgN(args, 1)
case "HSET", "HSETNX", "LREM", "LSET", "SETBIT", "SETEX", "PSETEX",
"SETRANGE", "ZINCRBY", "SMOVE", "RESTORE":
// Obfuscate 3rd argument:
// • HSET key field value
// • HSETNX key field value
// • LREM key count value
// • LSET key index value
// • SETBIT key offset value
// • SETEX key seconds value
// • PSETEX key milliseconds value
// • SETRANGE key offset value
// • ZINCRBY key increment member
// • SMOVE source destination member
// • RESTORE key ttl serialized-value [REPLACE]
obfuscateRedisArgN(args, 2)
case "LINSERT":
// Obfuscate 4th argument:
// • LINSERT key BEFORE|AFTER pivot value
obfuscateRedisArgN(args, 3)
case "GEOHASH", "GEOPOS", "GEODIST", "LPUSH", "RPUSH", "SREM",
"ZREM", "SADD":
// Obfuscate all arguments after the first one.
// • GEOHASH key member [member ...]
// • GEOPOS key member [member ...]
// • GEODIST key member1 member2 [unit]
// • LPUSH key value [value ...]
// • RPUSH key value [value ...]
// • SREM key member [member ...]
// • ZREM key member [member ...]
// • SADD key member [member ...]
if len(args) > 1 {
args[1] = "?"
args = args[:2]
}
case "GEOADD":
// Obfuscating every 3rd argument starting from first
// • GEOADD key longitude latitude member [longitude latitude member ...]
obfuscateRedisArgsStep(args, 1, 3)
case "HMSET":
// Every 2nd argument starting from first.
// • HMSET key field value [field value ...]
obfuscateRedisArgsStep(args, 1, 2)
case "MSET", "MSETNX":
// Every 2nd argument starting from command.
// • MSET key value [key value ...]
// • MSETNX key value [key value ...]
obfuscateRedisArgsStep(args, 0, 2)
case "CONFIG":
// Obfuscate 2nd argument to SET sub-command.
// • CONFIG SET parameter value
if strings.ToUpper(args[0]) == "SET" {
obfuscateRedisArgN(args, 2)
}
case "BITFIELD":
// Obfuscate 3rd argument to SET sub-command:
// • BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
var n int
for i, arg := range args {
if strings.ToUpper(arg) == "SET" {
n = i
}
if n > 0 && i-n == 3 {
args[i] = "?"
break
}
}
case "ZADD":
// Obfuscate every 2nd argument after potential optional ones.
// • ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
var i int
loop:
for i = range args {
if i == 0 {
continue // key
}
switch args[i] {
case "NX", "XX", "CH", "INCR":
// continue
default:
break loop
}
}
obfuscateRedisArgsStep(args, i, 2)
default:
// Obfuscate nothing.
}
out.WriteString(strings.Join(args, " "))
}
func obfuscateRedisArgN(args []string, n int) {
if len(args) > n {
args[n] = "?"
}
}
func obfuscateRedisArgsStep(args []string, start, step int) {
if start+step-1 >= len(args) {
// can't reach target
return
}
for i := start + step - 1; i < len(args); i += step {
args[i] = "?"
}
}

View File

@@ -0,0 +1,187 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"bytes"
"strings"
)
// redisTokenType specifies the token type returned by the tokenizer.
type redisTokenType int
const (
// redisTokenCommand is a command token. For compound tokens, it is
// only the first part up to a space.
redisTokenCommand redisTokenType = iota
// redisTokenArgument is an argument token.
redisTokenArgument
)
// String implements fmt.Stringer.
func (t redisTokenType) String() string {
return map[redisTokenType]string{
redisTokenCommand: "command",
redisTokenArgument: "argument",
}[t]
}
// redisTokenizer tokenizes a Redis command string. The string can be on
// multiple lines. The tokenizer is capable of parsing quoted strings and escape
// sequences inside them.
type redisTokenizer struct {
data []byte
ch byte
off int
done bool
state redisParseState
}
// redisParseState specifies the current state of the tokenizer.
type redisParseState int
const (
// redisStateCommand specifies that we are about to parse a command.
// It is usually the state at the beginning of the scan or after a
// new line.
redisStateCommand redisParseState = iota
// redisStateArgument specifies that we are about to parse an argument
// to a command or the rest of the tokens in a compound command.
redisStateArgument
)
// newRedisTokenizer returns a new tokenizer for the given data.
func newRedisTokenizer(data []byte) *redisTokenizer {
return &redisTokenizer{
data: bytes.TrimSpace(data),
off: -1,
state: redisStateCommand,
}
}
// scan returns the next token, it's type and a bool. The boolean specifies if
// the returned token was the last one.
func (t *redisTokenizer) scan() (tok string, typ redisTokenType, done bool) {
switch t.state {
case redisStateCommand:
return t.scanCommand()
default:
return t.scanArg()
}
}
// next advances the scanner to the next character.
func (t *redisTokenizer) next() {
t.off++
if t.off <= len(t.data)-1 {
t.ch = t.data[t.off]
return
}
t.done = true
}
// scanCommand scans a command from the buffer.
func (t *redisTokenizer) scanCommand() (tok string, typ redisTokenType, done bool) {
var (
str strings.Builder
started bool
)
for {
t.next()
if t.done {
return str.String(), typ, t.done
}
switch t.ch {
case ' ':
if !started {
// skip spaces preceding token
t.skipSpace()
break
}
// done scanning command
t.state = redisStateArgument
t.skipSpace()
return str.String(), redisTokenCommand, t.done
case '\n':
return str.String(), redisTokenCommand, t.done
default:
str.WriteByte(t.ch)
}
started = true
}
}
// scanArg scans an argument from the buffer.
func (t *redisTokenizer) scanArg() (tok string, typ redisTokenType, done bool) {
var (
str strings.Builder
quoted bool // in quoted string
escape bool // escape sequence
)
for {
t.next()
if t.done {
return str.String(), redisTokenArgument, t.done
}
switch t.ch {
case '\\':
str.WriteByte('\\')
if !escape {
// next character could be escaped
escape = true
continue
}
case '\n':
if !quoted {
// last argument, new command follows
t.state = redisStateCommand
return str.String(), redisTokenArgument, t.done
}
str.WriteByte('\n')
case '"':
str.WriteByte('"')
if !escape {
// this quote wasn't escaped, toggle quoted mode
quoted = !quoted
}
case ' ':
if !quoted {
t.skipSpace()
return str.String(), redisTokenArgument, t.done
}
str.WriteByte(' ')
default:
str.WriteByte(t.ch)
}
escape = false
}
}
// unread is the reverse of next, unreading a character.
func (t *redisTokenizer) unread() {
if t.off < 1 {
return
}
t.off--
t.ch = t.data[t.off]
}
// skipSpace moves the cursor forward until it meets the last space
// in a sequence of contiguous spaces.
func (t *redisTokenizer) skipSpace() {
for t.ch == ' ' || t.ch == '\t' || t.ch == '\r' && !t.done {
t.next()
}
if t.ch == '\n' {
// next token is a command
t.state = redisStateCommand
} else {
// don't steal the first non-space character
t.unread()
}
}

View File

@@ -0,0 +1,368 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
var questionMark = []byte("?")
// discardFilter is a token filter which discards certain elements from a query, such as
// comments and AS aliases by returning a nil buffer.
type discardFilter struct{ keepSQLAlias bool }
// Filter the given token so that a `nil` slice is returned if the token is in the token filtered list.
func (f *discardFilter) Filter(token, lastToken TokenKind, buffer []byte) (TokenKind, []byte, error) {
// filters based on previous token
switch lastToken {
case FilteredBracketedIdentifier:
if token != ']' {
// we haven't found the closing bracket yet, keep going
if token != ID {
// the token between the brackets *must* be an identifier,
// otherwise the query is invalid.
return LexError, nil, fmt.Errorf("expected identifier in bracketed filter, got %d", token)
}
return FilteredBracketedIdentifier, nil, nil
}
fallthrough
case As:
if token == '[' {
// the identifier followed by AS is an MSSQL bracketed identifier
// and will continue to be discarded until we find the corresponding
// closing bracket counter-part. See GitHub issue DataDog/datadog-trace-agent#475.
return FilteredBracketedIdentifier, nil, nil
}
if f.keepSQLAlias {
return token, buffer, nil
}
return Filtered, nil, nil
}
// filters based on the current token; if the next token should be ignored,
// return the same token value (not FilteredGroupable) and nil
switch token {
case Comment:
return Filtered, nil, nil
case ';':
return markFilteredGroupable(token), nil, nil
case As:
if !f.keepSQLAlias {
return As, nil, nil
}
fallthrough
default:
return token, buffer, nil
}
}
// Reset implements tokenFilter.
func (f *discardFilter) Reset() {}
// replaceFilter is a token filter which obfuscates strings and numbers in queries by replacing them
// with the "?" character.
type replaceFilter struct {
replaceDigits bool
}
// Filter the given token so that it will be replaced if in the token replacement list
func (f *replaceFilter) Filter(token, lastToken TokenKind, buffer []byte) (tokenType TokenKind, tokenBytes []byte, err error) {
switch lastToken {
case Savepoint:
return markFilteredGroupable(token), questionMark, nil
case '=':
switch token {
case DoubleQuotedString:
// double-quoted strings after assignments are eligible for obfuscation
return markFilteredGroupable(token), questionMark, nil
}
}
switch token {
case DollarQuotedString, String, Number, Null, Variable, PreparedStatement, BooleanLiteral, EscapeSequence:
return markFilteredGroupable(token), questionMark, nil
case '?':
// Cases like 'ARRAY [ ?, ? ]' should be collapsed into 'ARRAY [ ? ]'
return markFilteredGroupable(token), questionMark, nil
case TableName, ID:
if f.replaceDigits {
return token, replaceDigits(buffer), nil
}
fallthrough
default:
return token, buffer, nil
}
}
// Reset implements tokenFilter.
func (f *replaceFilter) Reset() {}
// groupingFilter is a token filter which groups together items replaced by the replaceFilter. It is meant
// to run immediately after it.
type groupingFilter struct {
groupFilter int // counts the number of values, e.g. 3 = ?, ?, ?
groupMulti int // counts the number of groups, e.g. 2 = (?, ?), (?, ?, ?)
}
// Filter the given token so that it will be discarded if a grouping pattern
// has been recognized. A grouping is composed by items like:
// * '( ?, ?, ? )'
// * '( ?, ? ), ( ?, ? )'
func (f *groupingFilter) Filter(token, lastToken TokenKind, buffer []byte) (tokenType TokenKind, tokenBytes []byte, err error) {
// increasing the number of groups means that we're filtering an entire group
// because it can be represented with a single '( ? )'
if (lastToken == '(' && isFilteredGroupable(token)) || (token == '(' && f.groupMulti > 0) {
f.groupMulti++
}
switch {
case f.groupMulti > 0 && lastToken == FilteredGroupableParenthesis && token == ID:
// this is the start of a new group that seems to be a nested query;
// cancel grouping.
f.Reset()
return token, append([]byte("( "), buffer...), nil
case isFilteredGroupable(token):
// the previous filter has dropped this token so we should start
// counting the group filter so that we accept only one '?' for
// the same group
f.groupFilter++
if f.groupFilter > 1 {
return markFilteredGroupable(token), nil, nil
}
case f.groupFilter > 0 && (token == ',' || token == '?'):
// if we are in a group drop all commas
return markFilteredGroupable(token), nil, nil
case f.groupMulti > 1:
// drop all tokens since we're in a counting group
// and they're duplicated
return markFilteredGroupable(token), nil, nil
case token != ',' && token != '(' && token != ')' && !isFilteredGroupable(token):
// when we're out of a group reset the filter state
f.Reset()
}
return token, buffer, nil
}
// isFilteredGroupable reports whether token is to be considered filtered groupable.
func isFilteredGroupable(token TokenKind) bool {
switch token {
case FilteredGroupable, FilteredGroupableParenthesis:
return true
default:
return false
}
}
// markFilteredGroupable returns the appropriate TokenKind to mark this token as
// filtered groupable.
func markFilteredGroupable(token TokenKind) TokenKind {
switch token {
case '(':
return FilteredGroupableParenthesis
default:
return FilteredGroupable
}
}
// Reset resets the groupingFilter so that it may be used again.
func (f *groupingFilter) Reset() {
f.groupFilter = 0
f.groupMulti = 0
}
// ObfuscateSQLString quantizes and obfuscates the given input SQL query string. Quantization removes
// some elements such as comments and aliases and obfuscation attempts to hide sensitive information
// in strings and numbers by redacting them.
func (o *Obfuscator) ObfuscateSQLString(in string) (*ObfuscatedQuery, error) {
return o.ObfuscateSQLStringWithOptions(in, &o.opts.SQL)
}
// ObfuscateSQLStringWithOptions accepts an optional SQLOptions to change the behavior of the obfuscator
// to quantize and obfuscate the given input SQL query string. Quantization removes some elements such as comments
// and aliases and obfuscation attempts to hide sensitive information in strings and numbers by redacting them.
func (o *Obfuscator) ObfuscateSQLStringWithOptions(in string, opts *SQLConfig) (*ObfuscatedQuery, error) {
if v, ok := o.queryCache.Get(in); ok {
return v.(*ObfuscatedQuery), nil
}
oq, err := o.obfuscateSQLString(in, opts)
if err != nil {
return oq, err
}
o.queryCache.Set(in, oq, oq.Cost())
return oq, nil
}
func (o *Obfuscator) obfuscateSQLString(in string, opts *SQLConfig) (*ObfuscatedQuery, error) {
lesc := o.useSQLLiteralEscapes()
tok := NewSQLTokenizer(in, lesc, opts)
out, err := attemptObfuscation(tok)
if err != nil && tok.SeenEscape() {
// If the tokenizer failed, but saw an escape character in the process,
// try again treating escapes differently
tok = NewSQLTokenizer(in, !lesc, opts)
if out, err2 := attemptObfuscation(tok); err2 == nil {
// If the second attempt succeeded, change the default behavior so that
// on the next run we get it right in the first run.
o.setSQLLiteralEscapes(!lesc)
return out, nil
}
}
return out, err
}
// tableFinderFilter is a filter which attempts to identify the table name as it goes through each
// token in a query.
type tableFinderFilter struct {
storeTableNames bool
// seen keeps track of unique table names encountered by the filter.
seen map[string]struct{}
// csv specifies a comma-separated list of tables
csv strings.Builder
}
// Filter implements tokenFilter.
func (f *tableFinderFilter) Filter(token, lastToken TokenKind, buffer []byte) (TokenKind, []byte, error) {
switch lastToken {
case From, Join:
// SELECT ... FROM [tableName]
// DELETE FROM [tableName]
// ... JOIN [tableName]
if r, _ := utf8.DecodeRune(buffer); !unicode.IsLetter(r) {
// first character in buffer is not a letter; we might have a nested
// query like SELECT * FROM (SELECT ...)
break
}
fallthrough
case Update, Into:
// UPDATE [tableName]
// INSERT INTO [tableName]
if f.storeTableNames {
f.storeName(string(buffer))
}
return TableName, buffer, nil
}
return token, buffer, nil
}
// storeName marks the given table name as seen in the internal storage.
func (f *tableFinderFilter) storeName(name string) {
if _, ok := f.seen[name]; ok {
return
}
if f.seen == nil {
f.seen = make(map[string]struct{}, 1)
}
f.seen[name] = struct{}{}
if f.csv.Len() > 0 {
f.csv.WriteByte(',')
}
f.csv.WriteString(name)
}
// CSV returns a comma-separated list of the tables seen by the filter.
func (f *tableFinderFilter) CSV() string { return f.csv.String() }
// Reset implements tokenFilter.
func (f *tableFinderFilter) Reset() {
for k := range f.seen {
delete(f.seen, k)
}
f.csv.Reset()
}
// ObfuscatedQuery specifies information about an obfuscated SQL query.
type ObfuscatedQuery struct {
Query string // the obfuscated SQL query
TablesCSV string // comma-separated list of tables that the query addresses
}
// Cost returns the number of bytes needed to store all the fields
// of this ObfuscatedQuery.
func (oq *ObfuscatedQuery) Cost() int64 {
return int64(len(oq.Query) + len(oq.TablesCSV))
}
// attemptObfuscation attempts to obfuscate the SQL query loaded into the tokenizer, using the given set of filters.
func attemptObfuscation(tokenizer *SQLTokenizer) (*ObfuscatedQuery, error) {
var (
storeTableNames = tokenizer.cfg.TableNames
out = bytes.NewBuffer(make([]byte, 0, len(tokenizer.buf)))
err error
lastToken TokenKind
discard = discardFilter{tokenizer.cfg.KeepSQLAlias}
replace = replaceFilter{replaceDigits: tokenizer.cfg.ReplaceDigits}
grouping groupingFilter
tableFinder = tableFinderFilter{storeTableNames: storeTableNames}
)
// call Scan() function until tokens are available or if a LEX_ERROR is raised. After
// retrieving a token, send it to the tokenFilter chains so that the token is discarded
// or replaced.
for {
token, buff := tokenizer.Scan()
if token == EndChar {
break
}
if token == LexError {
return nil, fmt.Errorf("%v", tokenizer.Err())
}
if token, buff, err = discard.Filter(token, lastToken, buff); err != nil {
return nil, err
}
if storeTableNames {
if token, buff, err = tableFinder.Filter(token, lastToken, buff); err != nil {
return nil, err
}
}
if token, buff, err = replace.Filter(token, lastToken, buff); err != nil {
return nil, err
}
if token, buff, err = grouping.Filter(token, lastToken, buff); err != nil {
return nil, err
}
if buff != nil {
if out.Len() != 0 {
switch token {
case ',':
case '=':
if lastToken == ':' {
// do not add a space before an equals if a colon was
// present before it.
break
}
fallthrough
default:
out.WriteRune(' ')
}
}
out.Write(buff)
}
lastToken = token
}
if out.Len() == 0 {
return nil, errors.New("result is empty")
}
return &ObfuscatedQuery{
Query: out.String(),
TablesCSV: tableFinder.CSV(),
}, nil
}
// ObfuscateSQLExecPlan obfuscates query conditions in the provided JSON encoded execution plan. If normalize=True,
// then cost and row estimates are also obfuscated away.
func (o *Obfuscator) ObfuscateSQLExecPlan(jsonPlan string, normalize bool) (string, error) {
if normalize {
return o.sqlExecPlanNormalize.obfuscate([]byte(jsonPlan))
}
return o.sqlExecPlan.obfuscate([]byte(jsonPlan))
}

View File

@@ -0,0 +1,767 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"bytes"
"fmt"
"unicode"
"unicode/utf8"
)
// tokenizer.go implemenents a lexer-like iterator that tokenizes SQL and CQL
// strings, so that an external component can filter or alter each token of the
// string. This implementation can't be used as a real SQL lexer (so a parser
// cannot build the AST) because many rules are ignored to make the tokenizer
// simpler.
// This implementation was inspired by https://github.com/youtube/vitess sql parser
// TODO: add the license to the NOTICE file
// TokenKind specifies the type of the token being scanned. It may be one of the defined
// constants below or in some cases the actual rune itself.
type TokenKind uint32
// EndChar is used to signal that the scanner has finished reading the query. This happens when
// there are no more characters left in the query or when invalid encoding is discovered. EndChar
// is an invalid rune value that can not be found in any valid string.
const EndChar = unicode.MaxRune + 1
// list of available tokens; this list has been reduced because we don't
// need a full-fledged tokenizer to implement a Lexer
const (
LexError = TokenKind(57346) + iota
ID
Limit
Null
String
DoubleQuotedString
DollarQuotedString // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
DollarQuotedFunc // a dollar-quoted string delimited by the tag "$func$"; gets special treatment when feature "dollar_quoted_func" is set
Number
BooleanLiteral
ValueArg
ListArg
Comment
Variable
Savepoint
PreparedStatement
EscapeSequence
NullSafeEqual
LE
GE
NE
Not
As
From
Update
Insert
Into
Join
TableName
ColonCast
// FilteredGroupable specifies that the given token has been discarded by one of the
// token filters and that it is groupable together with consecutive FilteredGroupable
// tokens.
FilteredGroupable
// FilteredGroupableParenthesis is a parenthesis marked as filtered groupable. It is the
// beginning of either a group of values ('(') or a nested query. We track is as
// a special case for when it may start a nested query as opposed to just another
// value group to be obfuscated.
FilteredGroupableParenthesis
// Filtered specifies that the token is a comma and was discarded by one
// of the filters.
Filtered
// FilteredBracketedIdentifier specifies that we are currently discarding
// a bracketed identifier (MSSQL).
// See issue https://github.com/DataDog/datadog-trace-agent/issues/475.
FilteredBracketedIdentifier
)
var tokenKindStrings = map[TokenKind]string{
LexError: "LexError",
ID: "ID",
Limit: "Limit",
Null: "Null",
String: "String",
DoubleQuotedString: "DoubleQuotedString",
DollarQuotedString: "DollarQuotedString",
DollarQuotedFunc: "DollarQuotedFunc",
Number: "Number",
BooleanLiteral: "BooleanLiteral",
ValueArg: "ValueArg",
ListArg: "ListArg",
Comment: "Comment",
Variable: "Variable",
Savepoint: "Savepoint",
PreparedStatement: "PreparedStatement",
EscapeSequence: "EscapeSequence",
NullSafeEqual: "NullSafeEqual",
LE: "LE",
GE: "GE",
NE: "NE",
Not: "NOT",
As: "As",
From: "From",
Update: "Update",
Insert: "Insert",
Into: "Into",
Join: "Join",
TableName: "TableName",
ColonCast: "ColonCast",
FilteredGroupable: "FilteredGroupable",
FilteredGroupableParenthesis: "FilteredGroupableParenthesis",
Filtered: "Filtered",
FilteredBracketedIdentifier: "FilteredBracketedIdentifier",
}
func (k TokenKind) String() string {
str, ok := tokenKindStrings[k]
if !ok {
return "<unknown>"
}
return str
}
const escapeCharacter = '\\'
// SQLTokenizer is the struct used to generate SQL
// tokens for the parser.
type SQLTokenizer struct {
pos int // byte offset of lastChar
lastChar rune // last read rune
buf []byte // buf holds the query that we are parsing
off int // off is the index into buf where the unread portion of the query begins.
err error // any error occurred while reading
curlys uint32 // number of active open curly braces in top-level SQL escape sequences.
literalEscapes bool // indicates we should not treat backslashes as escape characters
seenEscape bool // indicates whether this tokenizer has seen an escape character within a string
cfg *SQLConfig
}
// NewSQLTokenizer creates a new SQLTokenizer for the given SQL string. The literalEscapes argument specifies
// whether escape characters should be treated literally or as such.
func NewSQLTokenizer(sql string, literalEscapes bool, cfg *SQLConfig) *SQLTokenizer {
if cfg == nil {
cfg = new(SQLConfig)
}
return &SQLTokenizer{
buf: []byte(sql),
cfg: cfg,
literalEscapes: literalEscapes,
}
}
// Reset the underlying buffer and positions
func (tkn *SQLTokenizer) Reset(in string) {
tkn.pos = 0
tkn.lastChar = 0
tkn.buf = []byte(in)
tkn.off = 0
tkn.err = nil
}
// keywords used to recognize string tokens
var keywords = map[string]TokenKind{
"NULL": Null,
"TRUE": BooleanLiteral,
"FALSE": BooleanLiteral,
"SAVEPOINT": Savepoint,
"LIMIT": Limit,
"AS": As,
"FROM": From,
"UPDATE": Update,
"INSERT": Insert,
"INTO": Into,
"JOIN": Join,
}
// Err returns the last error that the tokenizer encountered, or nil.
func (tkn *SQLTokenizer) Err() error { return tkn.err }
func (tkn *SQLTokenizer) setErr(format string, args ...interface{}) {
if tkn.err != nil {
return
}
tkn.err = fmt.Errorf("at position %d: %v", tkn.pos, fmt.Errorf(format, args...))
}
// SeenEscape returns whether or not this tokenizer has seen an escape character within a scanned string
func (tkn *SQLTokenizer) SeenEscape() bool { return tkn.seenEscape }
// Scan scans the tokenizer for the next token and returns
// the token type and the token buffer.
func (tkn *SQLTokenizer) Scan() (TokenKind, []byte) {
if tkn.lastChar == 0 {
tkn.advance()
}
tkn.skipBlank()
switch ch := tkn.lastChar; {
case isLeadingLetter(ch):
return tkn.scanIdentifier()
case isDigit(ch):
return tkn.scanNumber(false)
default:
tkn.advance()
if tkn.lastChar == EndChar && tkn.err != nil {
// advance discovered an invalid encoding. We should return early.
return LexError, nil
}
switch ch {
case EndChar:
if tkn.err != nil {
return LexError, nil
}
return EndChar, nil
case ':':
if tkn.lastChar == ':' {
tkn.advance()
return ColonCast, []byte("::")
}
if unicode.IsSpace(tkn.lastChar) {
// example scenario: "autovacuum: VACUUM ANALYZE fake.table"
return TokenKind(ch), tkn.bytes()
}
if tkn.lastChar != '=' {
return tkn.scanBindVar()
}
fallthrough
case '~':
switch tkn.lastChar {
case '*':
tkn.advance()
return TokenKind('~'), []byte("~*")
default:
return TokenKind(ch), tkn.bytes()
}
case '=', ',', ';', '(', ')', '+', '*', '&', '|', '^', '[', ']', '?':
return TokenKind(ch), tkn.bytes()
case '.':
if isDigit(tkn.lastChar) {
return tkn.scanNumber(true)
}
return TokenKind(ch), tkn.bytes()
case '/':
switch tkn.lastChar {
case '/':
tkn.advance()
return tkn.scanCommentType1("//")
case '*':
tkn.advance()
return tkn.scanCommentType2()
default:
return TokenKind(ch), tkn.bytes()
}
case '-':
switch {
case tkn.lastChar == '-':
tkn.advance()
return tkn.scanCommentType1("--")
case isDigit(tkn.lastChar):
tkn.advance()
kind, tokenBytes := tkn.scanNumber(false)
return kind, append([]byte{'-'}, tokenBytes...)
default:
return TokenKind(ch), tkn.bytes()
}
case '#':
tkn.advance()
return tkn.scanCommentType1("#")
case '<':
switch tkn.lastChar {
case '>':
tkn.advance()
return NE, []byte("<>")
case '=':
tkn.advance()
switch tkn.lastChar {
case '>':
tkn.advance()
return NullSafeEqual, []byte("<=>")
default:
return LE, []byte("<=")
}
default:
return TokenKind(ch), tkn.bytes()
}
case '>':
if tkn.lastChar == '=' {
tkn.advance()
return GE, []byte(">=")
}
return TokenKind(ch), tkn.bytes()
case '!':
switch tkn.lastChar {
case '=':
tkn.advance()
return NE, []byte("!=")
case '~':
tkn.advance()
switch tkn.lastChar {
case '*':
tkn.advance()
return NE, []byte("!~*")
default:
return NE, []byte("!~")
}
default:
if isValidCharAfterOperator(tkn.lastChar) {
return Not, tkn.bytes()
}
tkn.setErr(`unexpected char "%c" (%d) after "!"`, tkn.lastChar, tkn.lastChar)
return LexError, tkn.bytes()
}
case '\'':
return tkn.scanString(ch, String)
case '"':
return tkn.scanString(ch, DoubleQuotedString)
case '`':
return tkn.scanString(ch, ID)
case '%':
if tkn.lastChar == '(' {
return tkn.scanVariableIdentifier('%')
}
if isLetter(tkn.lastChar) {
// format parameter (e.g. '%s')
return tkn.scanFormatParameter('%')
}
// modulo operator (e.g. 'id % 8')
return TokenKind(ch), tkn.bytes()
case '$':
if isDigit(tkn.lastChar) {
// TODO(gbbr): the first digit after $ does not necessarily guarantee
// that this isn't a dollar-quoted string constant. We might eventually
// want to cover for this use-case too (e.g. $1$some text$1$).
return tkn.scanPreparedStatement('$')
}
kind, tok := tkn.scanDollarQuotedString()
if kind == DollarQuotedFunc {
// this is considered an embedded query, we should try and
// obfuscate it
out, err := attemptObfuscation(NewSQLTokenizer(string(tok), tkn.literalEscapes, tkn.cfg))
if err != nil {
// if we can't obfuscate it, treat it as a regular string
return DollarQuotedString, tok
}
tok = append(append([]byte("$func$"), []byte(out.Query)...), []byte("$func$")...)
}
return kind, tok
case '{':
if tkn.pos == 1 || tkn.curlys > 0 {
// Do not fully obfuscate top-level SQL escape sequences like {{[?=]call procedure-name[([parameter][,parameter]...)]}.
// We want these to display a bit more context than just a plain '?'
// See: https://docs.oracle.com/cd/E13157_01/wlevs/docs30/jdbc_drivers/sqlescape.html
tkn.curlys++
return TokenKind(ch), tkn.bytes()
}
return tkn.scanEscapeSequence('{')
case '}':
if tkn.curlys == 0 {
// A closing curly brace has no place outside an in-progress top-level SQL escape sequence
// started by the '{' switch-case.
tkn.setErr(`unexpected byte %d`, ch)
return LexError, tkn.bytes()
}
tkn.curlys--
return TokenKind(ch), tkn.bytes()
default:
tkn.setErr(`unexpected byte %d`, ch)
return LexError, tkn.bytes()
}
}
}
func (tkn *SQLTokenizer) skipBlank() {
for unicode.IsSpace(tkn.lastChar) {
tkn.advance()
}
tkn.bytes()
}
// toUpper is a modified version of bytes.ToUpper. It returns an upper-cased version of the byte
// slice src with all Unicode letters mapped to their upper case. It is modified to also accept a
// byte slice dst as an argument, the underlying storage of which (up to the capacity of dst)
// will be used as the destination of the upper-case copy of src, if it fits. As a special case,
// toUpper will return src if the byte slice is already upper-case. This function is used rather
// than bytes.ToUpper to improve the memory performance of the obfuscator by saving unnecessary
// allocations happening in bytes.ToUpper
func toUpper(src, dst []byte) []byte {
dst = dst[:0]
isASCII, hasLower := true, false
for i := 0; i < len(src); i++ {
c := src[i]
if c >= utf8.RuneSelf {
isASCII = false
break
}
hasLower = hasLower || ('a' <= c && c <= 'z')
}
if cap(dst) < len(src) {
dst = make([]byte, 0, len(src))
}
if isASCII { // optimize for ASCII-only byte slices.
if !hasLower {
// Just return src.
return src
}
dst = dst[:len(src)]
for i := 0; i < len(src); i++ {
c := src[i]
if 'a' <= c && c <= 'z' {
c -= 'a' - 'A'
}
dst[i] = c
}
return dst
}
// This *could* be optimized, but it's an uncommon case.
return bytes.Map(unicode.ToUpper, src)
}
func (tkn *SQLTokenizer) scanIdentifier() (TokenKind, []byte) {
tkn.advance()
for isLetter(tkn.lastChar) || isDigit(tkn.lastChar) || tkn.lastChar == '.' || tkn.lastChar == '*' {
tkn.advance()
}
t := tkn.bytes()
// Space allows us to upper-case identifiers 256 bytes long or less without allocating heap
// storage for them, since space is allocated on the stack. A size of 256 bytes was chosen
// based on the allowed length of sql identifiers in various sql implementations.
var space [256]byte
upper := toUpper(t, space[:0])
if keywordID, found := keywords[string(upper)]; found {
return keywordID, t
}
return ID, t
}
func (tkn *SQLTokenizer) scanVariableIdentifier(prefix rune) (TokenKind, []byte) {
for tkn.advance(); tkn.lastChar != ')' && tkn.lastChar != EndChar; tkn.advance() {
}
tkn.advance()
if !isLetter(tkn.lastChar) {
tkn.setErr(`invalid character after variable identifier: "%c" (%d)`, tkn.lastChar, tkn.lastChar)
return LexError, tkn.bytes()
}
tkn.advance()
return Variable, tkn.bytes()
}
func (tkn *SQLTokenizer) scanFormatParameter(prefix rune) (TokenKind, []byte) {
tkn.advance()
return Variable, tkn.bytes()
}
// scanDollarQuotedString scans a Postgres dollar-quoted string constant.
// See: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
func (tkn *SQLTokenizer) scanDollarQuotedString() (TokenKind, []byte) {
kind, tag := tkn.scanString('$', String)
if kind == LexError {
return kind, tkn.bytes()
}
var (
got int
buf bytes.Buffer
)
delim := tag
// on empty strings, tkn.scanString returns the delimiters
if string(delim) != "$$" {
// on non-empty strings, the delimiter is $tag$
delim = append([]byte{'$'}, delim...)
delim = append(delim, '$')
}
for {
ch := tkn.lastChar
tkn.advance()
if ch == EndChar {
tkn.setErr("unexpected EOF in dollar-quoted string")
return LexError, buf.Bytes()
}
if byte(ch) == delim[got] {
got++
if got == len(delim) {
break
}
continue
}
if got > 0 {
_, err := buf.Write(delim[:got])
if err != nil {
tkn.setErr("error reading dollar-quoted string: %v", err)
return LexError, buf.Bytes()
}
got = 0
}
buf.WriteRune(ch)
}
if tkn.cfg.DollarQuotedFunc && string(delim) == "$func$" {
return DollarQuotedFunc, buf.Bytes()
}
return DollarQuotedString, buf.Bytes()
}
func (tkn *SQLTokenizer) scanPreparedStatement(prefix rune) (TokenKind, []byte) {
// a prepared statement expect a digit identifier like $1
if !isDigit(tkn.lastChar) {
tkn.setErr(`prepared statements must start with digits, got "%c" (%d)`, tkn.lastChar, tkn.lastChar)
return LexError, tkn.bytes()
}
// scanNumber keeps the prefix rune intact.
// read numbers and return an error if any
token, buff := tkn.scanNumber(false)
if token == LexError {
tkn.setErr("invalid number")
return LexError, tkn.bytes()
}
return PreparedStatement, buff
}
func (tkn *SQLTokenizer) scanEscapeSequence(braces rune) (TokenKind, []byte) {
for tkn.lastChar != '}' && tkn.lastChar != EndChar {
tkn.advance()
}
// we've reached the end of the string without finding
// the closing curly braces
if tkn.lastChar == EndChar {
tkn.setErr("unexpected EOF in escape sequence")
return LexError, tkn.bytes()
}
tkn.advance()
return EscapeSequence, tkn.bytes()
}
func (tkn *SQLTokenizer) scanBindVar() (TokenKind, []byte) {
token := ValueArg
if tkn.lastChar == ':' {
token = ListArg
tkn.advance()
}
if !isLetter(tkn.lastChar) {
tkn.setErr(`bind variables should start with letters, got "%c" (%d)`, tkn.lastChar, tkn.lastChar)
return LexError, tkn.bytes()
}
for isLetter(tkn.lastChar) || isDigit(tkn.lastChar) || tkn.lastChar == '.' {
tkn.advance()
}
return token, tkn.bytes()
}
func (tkn *SQLTokenizer) scanMantissa(base int) {
for digitVal(tkn.lastChar) < base {
tkn.advance()
}
}
func (tkn *SQLTokenizer) scanNumber(seenDecimalPoint bool) (TokenKind, []byte) {
if seenDecimalPoint {
tkn.scanMantissa(10)
goto exponent
}
if tkn.lastChar == '0' {
// int or float
tkn.advance()
if tkn.lastChar == 'x' || tkn.lastChar == 'X' {
// hexadecimal int
tkn.advance()
tkn.scanMantissa(16)
} else {
// octal int or float
seenDecimalDigit := false
tkn.scanMantissa(8)
if tkn.lastChar == '8' || tkn.lastChar == '9' {
// illegal octal int or float
seenDecimalDigit = true
tkn.scanMantissa(10)
}
if tkn.lastChar == '.' || tkn.lastChar == 'e' || tkn.lastChar == 'E' {
goto fraction
}
// octal int
if seenDecimalDigit {
// tkn.setErr called in caller
return LexError, tkn.bytes()
}
}
goto exit
}
// decimal int or float
tkn.scanMantissa(10)
fraction:
if tkn.lastChar == '.' {
tkn.advance()
tkn.scanMantissa(10)
}
exponent:
if tkn.lastChar == 'e' || tkn.lastChar == 'E' {
tkn.advance()
if tkn.lastChar == '+' || tkn.lastChar == '-' {
tkn.advance()
}
tkn.scanMantissa(10)
}
exit:
t := tkn.bytes()
if len(t) == 0 {
return LexError, nil
}
return Number, t
}
func (tkn *SQLTokenizer) scanString(delim rune, kind TokenKind) (TokenKind, []byte) {
buf := bytes.NewBuffer(tkn.buf[:0])
for {
ch := tkn.lastChar
tkn.advance()
if ch == delim {
if tkn.lastChar == delim {
// doubling a delimiter is the default way to embed the delimiter within a string
tkn.advance()
} else {
// a single delimiter denotes the end of the string
break
}
} else if ch == escapeCharacter {
tkn.seenEscape = true
if !tkn.literalEscapes {
// treat as an escape character
ch = tkn.lastChar
tkn.advance()
}
}
if ch == EndChar {
tkn.setErr("unexpected EOF in string")
return LexError, buf.Bytes()
}
buf.WriteRune(ch)
}
if kind == ID && buf.Len() == 0 || bytes.IndexFunc(buf.Bytes(), func(r rune) bool { return !unicode.IsSpace(r) }) == -1 {
// This string is an empty or white-space only identifier.
// We should keep the start and end delimiters in order to
// avoid creating invalid queries.
// See: https://github.com/DataDog/datadog-trace-agent/issues/316
return kind, append(runeBytes(delim), runeBytes(delim)...)
}
return kind, buf.Bytes()
}
func (tkn *SQLTokenizer) scanCommentType1(prefix string) (TokenKind, []byte) {
for tkn.lastChar != EndChar {
if tkn.lastChar == '\n' {
tkn.advance()
break
}
tkn.advance()
}
return Comment, tkn.bytes()
}
func (tkn *SQLTokenizer) scanCommentType2() (TokenKind, []byte) {
for {
if tkn.lastChar == '*' {
tkn.advance()
if tkn.lastChar == '/' {
tkn.advance()
break
}
continue
}
if tkn.lastChar == EndChar {
tkn.setErr("unexpected EOF in comment")
return LexError, tkn.bytes()
}
tkn.advance()
}
return Comment, tkn.bytes()
}
// advance advances the tokenizer to the next rune. If the decoder encounters an error decoding, or
// the end of the buffer is reached, tkn.lastChar will be set to EndChar. In case of a decoding
// error, tkn.err will also be set.
func (tkn *SQLTokenizer) advance() {
ch, n := utf8.DecodeRune(tkn.buf[tkn.off:])
if ch == utf8.RuneError && n < 2 {
tkn.pos++
tkn.lastChar = EndChar
if n == 1 {
tkn.setErr("invalid UTF-8 encoding beginning with 0x%x", tkn.buf[tkn.off])
}
return
}
if tkn.lastChar != 0 || tkn.pos > 0 {
// we are past the first character
tkn.pos += n
}
tkn.off += n
tkn.lastChar = ch
}
// bytes returns all the bytes that were advanced over since its last call.
// This excludes tkn.lastChar, which will remain in the buffer
func (tkn *SQLTokenizer) bytes() []byte {
if tkn.lastChar == EndChar {
ret := tkn.buf[:tkn.off]
tkn.buf = tkn.buf[tkn.off:]
tkn.off = 0
return ret
}
lastLen := utf8.RuneLen(tkn.lastChar)
ret := tkn.buf[:tkn.off-lastLen]
tkn.buf = tkn.buf[tkn.off-lastLen:]
tkn.off = lastLen
return ret
}
func isLeadingLetter(ch rune) bool {
return unicode.IsLetter(ch) || ch == '_' || ch == '@'
}
func isLetter(ch rune) bool {
return isLeadingLetter(ch) || ch == '#'
}
func digitVal(ch rune) int {
switch {
case '0' <= ch && ch <= '9':
return int(ch) - '0'
case 'a' <= ch && ch <= 'f':
return int(ch) - 'a' + 10
case 'A' <= ch && ch <= 'F':
return int(ch) - 'A' + 10
}
return 16 // larger than any legal digit val
}
func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' }
// runeBytes converts the given rune to a slice of bytes.
func runeBytes(r rune) []byte {
buf := make([]byte, utf8.UTFMax)
n := utf8.EncodeRune(buf, r)
return buf[:n]
}
// isValidCharAfterOperator returns true if c is a valid character after an operator
func isValidCharAfterOperator(c rune) bool {
return c == '(' || c == '`' || c == '\'' || c == '"' || c == '+' || c == '-' || unicode.IsSpace(c) || isLetter(c) || isDigit(c)
}

View File

@@ -0,0 +1,200 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016-present Datadog, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,336 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022-present Datadog, Inc.
package state
import (
"encoding/json"
"errors"
"fmt"
"github.com/DataDog/go-tuf/data"
)
/*
To add support for a new product:
1. Add the definition of the product to the const() block of products and the `allProducts` list.
2. Define the serialized configuration struct as well as a function to parse the config from a []byte.
3. Add the product to the `parseConfig` function
4. Add a method on the `Repository` to retrieved typed configs for the product.
*/
var allProducts = []string{ProductAPMSampling, ProductCWSDD, ProductASMFeatures, ProductASMDD, ProductASMData}
const (
// ProductAPMSampling is the apm sampling product
ProductAPMSampling = "APM_SAMPLING"
// ProductCWSDD is the cloud workload security product managed by datadog employees
ProductCWSDD = "CWS_DD"
// ProductASMFeatures is the ASM product used form ASM activation through remote config
ProductASMFeatures = "ASM_FEATURES"
// ProductASMDD is the application security monitoring product managed by datadog employees
ProductASMDD = "ASM_DD"
// ProductASMData is the ASM product used to configure WAF rules data
ProductASMData = "ASM_DATA"
)
// ErrNoConfigVersion occurs when a target file's custom meta is missing the config version
var ErrNoConfigVersion = errors.New("version missing in custom file meta")
func parseConfig(product string, raw []byte, metadata Metadata) (interface{}, error) {
var c interface{}
var err error
switch product {
case ProductAPMSampling:
c, err = parseConfigAPMSampling(raw, metadata)
case ProductASMFeatures:
c, err = parseASMFeaturesConfig(raw, metadata)
case ProductCWSDD:
c, err = parseConfigCWSDD(raw, metadata)
case ProductASMDD:
c, err = parseConfigASMDD(raw, metadata)
case ProductASMData:
c, err = parseConfigASMData(raw, metadata)
default:
return nil, fmt.Errorf("unknown product - %s", product)
}
return c, err
}
// APMSamplingConfig is a deserialized APM Sampling configuration file
// along with its associated remote config metadata.
type APMSamplingConfig struct {
Config []byte
Metadata Metadata
}
func parseConfigAPMSampling(data []byte, metadata Metadata) (APMSamplingConfig, error) {
// We actually don't parse the payload here, we delegate this responsibility to the trace agent
return APMSamplingConfig{
Config: data,
Metadata: metadata,
}, nil
}
// APMConfigs returns the currently active APM configs
func (r *Repository) APMConfigs() map[string]APMSamplingConfig {
typedConfigs := make(map[string]APMSamplingConfig)
configs := r.getConfigs(ProductAPMSampling)
for path, conf := range configs {
// We control this, so if this has gone wrong something has gone horribly wrong
typed, ok := conf.(APMSamplingConfig)
if !ok {
panic("unexpected config stored as APMSamplingConfig")
}
typedConfigs[path] = typed
}
return typedConfigs
}
// ConfigCWSDD is a deserialized CWS DD configuration file along with its
// associated remote config metadata
type ConfigCWSDD struct {
Config []byte
Metadata Metadata
}
func parseConfigCWSDD(data []byte, metadata Metadata) (ConfigCWSDD, error) {
return ConfigCWSDD{
Config: data,
Metadata: metadata,
}, nil
}
// CWSDDConfigs returns the currently active CWSDD config files
func (r *Repository) CWSDDConfigs() map[string]ConfigCWSDD {
typedConfigs := make(map[string]ConfigCWSDD)
configs := r.getConfigs(ProductCWSDD)
for path, conf := range configs {
// We control this, so if this has gone wrong something has gone horribly wrong
typed, ok := conf.(ConfigCWSDD)
if !ok {
panic("unexpected config stored as CWSDD Config")
}
typedConfigs[path] = typed
}
return typedConfigs
}
// ConfigASMDD is a deserialized ASM DD configuration file along with its
// associated remote config metadata
type ConfigASMDD struct {
Config []byte
Metadata Metadata
}
func parseConfigASMDD(data []byte, metadata Metadata) (ConfigASMDD, error) {
return ConfigASMDD{
Config: data,
Metadata: metadata,
}, nil
}
// ASMDDConfigs returns the currently active ASMDD configs
func (r *Repository) ASMDDConfigs() map[string]ConfigASMDD {
typedConfigs := make(map[string]ConfigASMDD)
configs := r.getConfigs(ProductASMDD)
for path, conf := range configs {
// We control this, so if this has gone wrong something has gone horribly wrong
typed, ok := conf.(ConfigASMDD)
if !ok {
panic("unexpected config stored as ASMDD Config")
}
typedConfigs[path] = typed
}
return typedConfigs
}
// ASMFeaturesConfig is a deserialized configuration file that indicates whether ASM should be enabled
// within a tracer, along with its associated remote config metadata.
type ASMFeaturesConfig struct {
Config ASMFeaturesData
Metadata Metadata
}
// ASMFeaturesData describes the enabled state of ASM features
type ASMFeaturesData struct {
ASM struct {
Enabled bool `json:"enabled"`
} `json:"asm"`
}
func parseASMFeaturesConfig(data []byte, metadata Metadata) (ASMFeaturesConfig, error) {
var f ASMFeaturesData
err := json.Unmarshal(data, &f)
if err != nil {
return ASMFeaturesConfig{}, nil
}
return ASMFeaturesConfig{
Config: f,
Metadata: metadata,
}, nil
}
// ASMFeaturesConfigs returns the currently active ASMFeatures configs
func (r *Repository) ASMFeaturesConfigs() map[string]ASMFeaturesConfig {
typedConfigs := make(map[string]ASMFeaturesConfig)
configs := r.getConfigs(ProductASMFeatures)
for path, conf := range configs {
// We control this, so if this has gone wrong something has gone horribly wrong
typed, ok := conf.(ASMFeaturesConfig)
if !ok {
panic("unexpected config stored as ASMFeaturesConfig")
}
typedConfigs[path] = typed
}
return typedConfigs
}
// ApplyState represents the status of a configuration application by a remote configuration client
// Clients need to either ack the correct application of received configurations, or communicate that
// they haven't applied it yet, or communicate any error that may have happened while doing so
type ApplyState uint64
const (
ApplyStateUnknown ApplyState = iota
ApplyStateUnacknowledged
ApplyStateAcknowledged
ApplyStateError
)
// ApplyStatus is the processing status for a given configuration.
// It basically represents whether a config was successfully processed and apply, or if an error occurred
type ApplyStatus struct {
State ApplyState
Error string
}
// ASMDataConfig is a deserialized configuration file that holds rules data that can be used
// by the ASM WAF for specific features (example: ip blocking).
type ASMDataConfig struct {
Config ASMDataRulesData
Metadata Metadata
}
// ASMDataRulesData is a serializable array of rules data entries
type ASMDataRulesData struct {
RulesData []ASMDataRuleData `json:"rules_data"`
}
// ASMDataRuleData is an entry in the rules data list held by an ASMData configuration
type ASMDataRuleData struct {
ID string `json:"id"`
Type string `json:"type"`
Data []ASMDataRuleDataEntry `json:"data"`
}
// ASMDataRuleDataEntry represents a data entry in a rule data file
type ASMDataRuleDataEntry struct {
Expiration int64 `json:"expiration,omitempty"`
Value string `json:"value"`
}
func parseConfigASMData(data []byte, metadata Metadata) (ASMDataConfig, error) {
cfg := ASMDataConfig{
Metadata: metadata,
}
err := json.Unmarshal(data, &cfg.Config)
return cfg, err
}
// ASMDataConfigs returns the currently active ASMData configs
func (r *Repository) ASMDataConfigs() map[string]ASMDataConfig {
typedConfigs := make(map[string]ASMDataConfig)
configs := r.getConfigs(ProductASMData)
for path, cfg := range configs {
// We control this, so if this has gone wrong something has gone horribly wrong
typed, ok := cfg.(ASMDataConfig)
if !ok {
panic("unexpected config stored as ASMDataConfig")
}
typedConfigs[path] = typed
}
return typedConfigs
}
// Metadata stores remote config metadata for a given configuration
type Metadata struct {
Product string
ID string
Name string
Version uint64
RawLength uint64
Hashes map[string][]byte
ApplyStatus ApplyStatus
}
func newConfigMetadata(parsedPath configPath, tfm data.TargetFileMeta) (Metadata, error) {
var m Metadata
m.ID = parsedPath.ConfigID
m.Product = parsedPath.Product
m.Name = parsedPath.Name
m.RawLength = uint64(tfm.Length)
m.Hashes = make(map[string][]byte)
for k, v := range tfm.Hashes {
m.Hashes[k] = []byte(v)
}
v, err := fileMetaVersion(tfm)
if err != nil {
return Metadata{}, err
}
m.Version = v
return m, nil
}
type fileMetaCustom struct {
Version *uint64 `json:"v"`
}
func fileMetaVersion(fm data.TargetFileMeta) (uint64, error) {
if fm.Custom == nil {
return 0, ErrNoConfigVersion
}
fmc, err := parseFileMetaCustom(*fm.Custom)
if err != nil {
return 0, err
}
return *fmc.Version, nil
}
func parseFileMetaCustom(rawCustom []byte) (fileMetaCustom, error) {
var custom fileMetaCustom
err := json.Unmarshal(rawCustom, &custom)
if err != nil {
return fileMetaCustom{}, err
}
if custom.Version == nil {
return fileMetaCustom{}, ErrNoConfigVersion
}
return custom, nil
}

View File

@@ -0,0 +1,100 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022-present Datadog, Inc.
package state
import (
"fmt"
"regexp"
"strconv"
"strings"
)
var (
// matches datadog/<int>/<string>/<string>/<string> for datadog/<org_id>/<product>/<config_id>/<file>
datadogPathRegexp = regexp.MustCompile(`^datadog/(\d+)/([^/]+)/([^/]+)/([^/]+)$`)
datadogPathRegexpGroups = 4
// matches employee/<string>/<string>/<string> for employee/<org_id>/<product>/<config_id>/<file>
employeePathRegexp = regexp.MustCompile(`^employee/([^/]+)/([^/]+)/([^/]+)$`)
employeePathRegexpGroups = 3
)
type source uint
const (
sourceUnknown source = iota
sourceDatadog
sourceEmployee
)
type configPath struct {
Source source
OrgID int64
Product string
ConfigID string
Name string
}
func parseConfigPath(path string) (configPath, error) {
configType := parseConfigPathSource(path)
switch configType {
case sourceDatadog:
return parseDatadogConfigPath(path)
case sourceEmployee:
return parseEmployeeConfigPath(path)
}
return configPath{}, fmt.Errorf("config path '%s' has unknown source", path)
}
func parseDatadogConfigPath(path string) (configPath, error) {
matchedGroups := datadogPathRegexp.FindStringSubmatch(path)
if len(matchedGroups) != datadogPathRegexpGroups+1 {
return configPath{}, fmt.Errorf("config file path '%s' has wrong format", path)
}
rawOrgID := matchedGroups[1]
orgID, err := strconv.ParseInt(rawOrgID, 10, 64)
if err != nil {
return configPath{}, fmt.Errorf("could not parse orgID '%s' in config file path: %v", rawOrgID, err)
}
rawProduct := matchedGroups[2]
if len(rawProduct) == 0 {
return configPath{}, fmt.Errorf("product is empty")
}
return configPath{
Source: sourceDatadog,
OrgID: orgID,
Product: rawProduct,
ConfigID: matchedGroups[3],
Name: matchedGroups[4],
}, nil
}
func parseEmployeeConfigPath(path string) (configPath, error) {
matchedGroups := employeePathRegexp.FindStringSubmatch(path)
if len(matchedGroups) != employeePathRegexpGroups+1 {
return configPath{}, fmt.Errorf("config file path '%s' has wrong format", path)
}
rawProduct := matchedGroups[1]
if len(rawProduct) == 0 {
return configPath{}, fmt.Errorf("product is empty")
}
return configPath{
Source: sourceEmployee,
Product: rawProduct,
ConfigID: matchedGroups[2],
Name: matchedGroups[3],
}, nil
}
func parseConfigPathSource(path string) source {
switch {
case strings.HasPrefix(path, "datadog/"):
return sourceDatadog
case strings.HasPrefix(path, "employee/"):
return sourceEmployee
}
return sourceUnknown
}

View File

@@ -0,0 +1,418 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022-present Datadog, Inc.
package state
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"github.com/DataDog/go-tuf/data"
)
var (
// ErrMalformedEmbeddedRoot occurs when the TUF root provided is invalid
ErrMalformedEmbeddedRoot = errors.New("malformed embedded TUF root file provided")
)
// RepositoryState contains all of the information about the current config files
// stored by the client to be able to make an update request to an Agent
type RepositoryState struct {
Configs []ConfigState
CachedFiles []CachedFile
TargetsVersion int64
RootsVersion int64
OpaqueBackendState []byte
}
// ConfigState describes an applied config by the agent client.
type ConfigState struct {
Product string
ID string
Version uint64
ApplyStatus ApplyStatus
}
// CachedFile describes a cached file stored by the agent client
//
// Note: You may be wondering why this exists when `ConfigState` exists
// as well. The API for requesting updates does not mandate that a client
// cache config files. This implementation just happens to do so.
type CachedFile struct {
Path string
Length uint64
Hashes map[string][]byte
}
// An Update contains all the data needed to update a client's remote config repository state
type Update struct {
// TUFRoots contains, in order, updated roots that this repository needs to keep up with TUF validation
TUFRoots [][]byte
// TUFTargets is the latest TUF Targets file and is used to validate raw config files
TUFTargets []byte
// TargetFiles stores the raw config files by their full TUF path
TargetFiles map[string][]byte
// ClientcConfigs is a list of TUF path's corresponding to config files designated for this repository
ClientConfigs []string
}
// Repository is a remote config client used in a downstream process to retrieve
// remote config updates from an Agent.
type Repository struct {
// TUF related data
latestTargets *data.Targets
tufRootsClient *tufRootsClient
opaqueBackendState []byte
// Unverified mode
tufVerificationEnabled bool
latestRootVersion int64
// Config file storage
metadata map[string]Metadata
configs map[string]map[string]interface{}
}
// NewRepository creates a new remote config repository that will track
// both TUF metadata and raw config files for a client.
func NewRepository(embeddedRoot []byte) (*Repository, error) {
if embeddedRoot == nil {
return nil, ErrMalformedEmbeddedRoot
}
configs := make(map[string]map[string]interface{})
for _, product := range allProducts {
configs[product] = make(map[string]interface{})
}
tufRootsClient, err := newTufRootsClient(embeddedRoot)
if err != nil {
return nil, err
}
return &Repository{
latestTargets: data.NewTargets(),
tufRootsClient: tufRootsClient,
metadata: make(map[string]Metadata),
configs: configs,
tufVerificationEnabled: true,
}, nil
}
// NewUnverifiedRepository creates a new remote config repository that will
// track config files for a client WITHOUT verifying any TUF related metadata.
func NewUnverifiedRepository() (*Repository, error) {
configs := make(map[string]map[string]interface{})
for _, product := range allProducts {
configs[product] = make(map[string]interface{})
}
return &Repository{
latestTargets: data.NewTargets(),
metadata: make(map[string]Metadata),
configs: configs,
tufVerificationEnabled: false,
}, nil
}
// Update processes the ClientGetConfigsResponse from the Agent and updates the
// configuration state
func (r *Repository) Update(update Update) ([]string, error) {
var err error
var updatedTargets *data.Targets
var tmpRootClient *tufRootsClient
// TUF: Update the roots and verify the TUF Targets file (optional)
//
// We don't want to partially update the state, so we need a temporary client to hold the new root
// data until we know it's valid. Since verification is optional, if the repository was configured
// to not do TUF verification we only deserialize the TUF targets file.
if r.tufVerificationEnabled {
tmpRootClient, err = r.tufRootsClient.clone()
if err != nil {
return nil, err
}
err = tmpRootClient.updateRoots(update.TUFRoots)
if err != nil {
return nil, err
}
updatedTargets, err = tmpRootClient.validateTargets(update.TUFTargets)
if err != nil {
return nil, err
}
} else {
updatedTargets, err = unsafeUnmarshalTargets(update.TUFTargets)
if err != nil {
return nil, err
}
}
clientConfigsMap := make(map[string]struct{})
for _, f := range update.ClientConfigs {
clientConfigsMap[f] = struct{}{}
}
result := newUpdateResult()
// 2: Check the config list and mark any missing configs as "to be removed"
for _, configs := range r.configs {
for path := range configs {
if _, ok := clientConfigsMap[path]; !ok {
result.removed = append(result.removed, path)
}
}
}
// 3: For all the files referenced in this update
for _, path := range update.ClientConfigs {
targetFileMetadata, ok := updatedTargets.Targets[path]
if !ok {
return nil, fmt.Errorf("missing config file in TUF targets - %s", path)
}
// 3.a: Extract the product and ID from the path
parsedPath, err := parseConfigPath(path)
if err != nil {
return nil, err
}
storedMetadata, exists := r.metadata[path]
if exists && hashesEqual(targetFileMetadata.Hashes, storedMetadata.Hashes) {
continue
}
// 3.d: Ensure that the raw configuration file is present in the
// update payload.
raw, ok := update.TargetFiles[path]
if !ok {
return nil, fmt.Errorf("missing update file - %s", path)
}
// TUF: Validate the hash of the raw target file and ensure that it matches
// the TUF metadata
err = validateTargetFileHash(targetFileMetadata, raw)
if err != nil {
return nil, fmt.Errorf("error validating %s hash with TUF metadata - %v", path, err)
}
// 3.e: Deserialize the configuration.
// 3.f: Store the update details for application later
//
// Note: We don't have to worry about extra fields as mentioned
// in the RFC because the encoding/json library handles that for us.
m, err := newConfigMetadata(parsedPath, targetFileMetadata)
if err != nil {
return nil, err
}
config, err := parseConfig(parsedPath.Product, raw, m)
if err != nil {
return nil, err
}
result.metadata[path] = m
result.changed[parsedPath.Product][path] = config
}
// 4.a: Store the new targets.signed.custom.opaque_client_state
// TUF: Store the updated roots now that everything has validated
if r.tufVerificationEnabled {
r.tufRootsClient = tmpRootClient
} else if update.TUFRoots != nil && len(update.TUFRoots) > 0 {
v, err := extractRootVersion(update.TUFRoots[len(update.TUFRoots)-1])
if err != nil {
return nil, err
}
r.latestRootVersion = v
}
r.latestTargets = updatedTargets
if r.latestTargets.Custom != nil {
r.opaqueBackendState = extractOpaqueBackendState(*r.latestTargets.Custom)
}
// Upstream may not want to take any actions if the update result doesn't
// change any configs.
if result.isEmpty() {
return nil, nil
}
changedProducts := make([]string, 0)
for product, configs := range result.changed {
if len(configs) > 0 {
changedProducts = append(changedProducts, product)
}
}
// 4.b/4.rave the new state and apply cleanups
r.applyUpdateResult(update, result)
return changedProducts, nil
}
// UpdateApplyStatus updates the config's metadata to reflect its processing state
// Can be used after a call to Update() in order to tell the repository which config was acked, which
// wasn't and which errors occurred while processing.
// Note: it is the responsibility of the caller to ensure that no new Update() call was made between
// the first Update() call and the call to UpdateApplyStatus() so as to keep the repository state accurate.
func (r *Repository) UpdateApplyStatus(cfgPath string, status ApplyStatus) {
if m, ok := r.metadata[cfgPath]; ok {
m.ApplyStatus = status
}
}
func (r *Repository) getConfigs(product string) map[string]interface{} {
configs, ok := r.configs[product]
if !ok {
return nil
}
return configs
}
// applyUpdateResult changes the state of the client based on the given update.
//
// The update is guaranteed to succeed at this point, having been vetted and the details
// needed to apply the update stored in the `updateResult`.
func (r *Repository) applyUpdateResult(update Update, result updateResult) {
// 4.b Save all the updated and new config files
for product, configs := range result.changed {
for path, config := range configs {
m := r.configs[product]
m[path] = config
}
}
for path, metadata := range result.metadata {
r.metadata[path] = metadata
}
// 5.b Clean up the cache of any removed configs
for _, path := range result.removed {
delete(r.metadata, path)
for _, configs := range r.configs {
delete(configs, path)
}
}
}
// CurrentState returns all of the information needed to
// make an update for new configurations.
func (r *Repository) CurrentState() (RepositoryState, error) {
var configs []ConfigState
var cached []CachedFile
for path, metadata := range r.metadata {
configs = append(configs, configStateFromMetadata(metadata))
cached = append(cached, cachedFileFromMetadata(path, metadata))
}
var latestRootVersion int64
if r.tufVerificationEnabled {
root, err := r.tufRootsClient.latestRoot()
if err != nil {
return RepositoryState{}, err
}
latestRootVersion = root.Version
} else {
latestRootVersion = r.latestRootVersion
}
return RepositoryState{
Configs: configs,
CachedFiles: cached,
TargetsVersion: r.latestTargets.Version,
RootsVersion: latestRootVersion,
OpaqueBackendState: r.opaqueBackendState,
}, nil
}
// An updateResult allows the client to apply the update as a transaction
// after validating all required preconditions
type updateResult struct {
removed []string
metadata map[string]Metadata
changed map[string]map[string]interface{}
}
func newUpdateResult() updateResult {
changed := make(map[string]map[string]interface{})
for _, p := range allProducts {
changed[p] = make(map[string]interface{})
}
return updateResult{
removed: make([]string, 0),
metadata: make(map[string]Metadata),
changed: changed,
}
}
func (ur updateResult) Log() {
log.Printf("Removed Configs: %v", ur.removed)
var b strings.Builder
b.WriteString("Changed configs: [")
for path := range ur.metadata {
b.WriteString(path)
b.WriteString(" ")
}
b.WriteString("]")
log.Println(b.String())
}
func (ur updateResult) isEmpty() bool {
return len(ur.removed) == 0 && len(ur.metadata) == 0
}
func configStateFromMetadata(m Metadata) ConfigState {
return ConfigState{
Product: m.Product,
ID: m.ID,
Version: m.Version,
ApplyStatus: m.ApplyStatus,
}
}
func cachedFileFromMetadata(path string, m Metadata) CachedFile {
return CachedFile{
Path: path,
Length: m.RawLength,
Hashes: m.Hashes,
}
}
// hashesEqual checks if the hash values in the TUF metadata file match the stored
// hash values for a given config
func hashesEqual(tufHashes data.Hashes, storedHashes map[string][]byte) bool {
for algorithm, value := range tufHashes {
v, ok := storedHashes[algorithm]
if !ok {
continue
}
if !bytes.Equal(value, v) {
return false
}
}
return true
}
func extractOpaqueBackendState(targetsCustom []byte) []byte {
state := struct {
State []byte `json:"opaque_backend_state"`
}{nil}
err := json.Unmarshal(targetsCustom, &state)
if err != nil {
return []byte{}
}
return state.State
}

View File

@@ -0,0 +1,233 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022-present Datadog, Inc.
package state
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"github.com/DataDog/go-tuf/client"
"github.com/DataDog/go-tuf/data"
"github.com/DataDog/go-tuf/util"
"github.com/DataDog/go-tuf/verify"
)
type tufRootsClient struct {
rootClient *client.Client
rootLocalStore client.LocalStore
rootRemoteStore *rootClientRemoteStore
}
func newTufRootsClient(root []byte) (*tufRootsClient, error) {
rootLocalStore := client.MemoryLocalStore()
rootRemoteStore := &rootClientRemoteStore{}
rootClient := client.NewClient(rootLocalStore, rootRemoteStore)
err := rootClient.InitLocal(root)
if err != nil {
return nil, err
}
return &tufRootsClient{
rootClient: rootClient,
rootLocalStore: rootLocalStore,
rootRemoteStore: rootRemoteStore,
}, nil
}
func (trc *tufRootsClient) clone() (*tufRootsClient, error) {
root, err := trc.latestRootRaw()
if err != nil {
return nil, err
}
return newTufRootsClient(root)
}
func (trc *tufRootsClient) updateRoots(newRoots [][]byte) error {
if len(newRoots) == 0 {
return nil
}
trc.rootRemoteStore.roots = append(trc.rootRemoteStore.roots, newRoots...)
return trc.rootClient.UpdateRoots()
}
func (trc *tufRootsClient) latestRoot() (*data.Root, error) {
raw, err := trc.latestRootRaw()
if err != nil {
return nil, err
}
return unsafeUnmarshalRoot(raw)
}
func (trc *tufRootsClient) latestRootRaw() ([]byte, error) {
metas, err := trc.rootLocalStore.GetMeta()
if err != nil {
return nil, err
}
rawRoot := metas["root.json"]
return rawRoot, nil
}
func (trc *tufRootsClient) validateTargets(rawTargets []byte) (*data.Targets, error) {
root, err := trc.latestRoot()
if err != nil {
return nil, err
}
db := verify.NewDB()
for _, key := range root.Keys {
for _, id := range key.IDs() {
if err := db.AddKey(id, key); err != nil {
return nil, err
}
}
}
targetsRole, hasRoleTargets := root.Roles["targets"]
if !hasRoleTargets {
return nil, fmt.Errorf("root is missing a targets role")
}
role := &data.Role{Threshold: targetsRole.Threshold, KeyIDs: targetsRole.KeyIDs}
if err := db.AddRole("targets", role); err != nil {
return nil, fmt.Errorf("could not add targets role to db: %v", err)
}
var targets data.Targets
err = db.Unmarshal(rawTargets, &targets, "targets", 0)
if err != nil {
return nil, err
}
return &targets, nil
}
type rootClientRemoteStore struct {
roots [][]byte
}
func (s *rootClientRemoteStore) GetMeta(name string) (stream io.ReadCloser, size int64, err error) {
metaPath, err := parseMetaPath(name)
if err != nil {
return nil, 0, err
}
if metaPath.role != roleRoot || !metaPath.versionSet {
return nil, 0, client.ErrNotFound{File: name}
}
for _, root := range s.roots {
parsedRoot, err := unsafeUnmarshalRoot(root)
if err != nil {
return nil, 0, err
}
if parsedRoot.Version == metaPath.version {
return io.NopCloser(bytes.NewReader(root)), int64(len(root)), nil
}
}
return nil, 0, client.ErrNotFound{File: name}
}
func (s *rootClientRemoteStore) GetTarget(path string) (stream io.ReadCloser, size int64, err error) {
return nil, 0, client.ErrNotFound{File: path}
}
type role string
const (
roleRoot role = "root"
)
type metaPath struct {
role role
version int64
versionSet bool
}
func parseMetaPath(rawMetaPath string) (metaPath, error) {
splitRawMetaPath := strings.SplitN(rawMetaPath, ".", 3)
if len(splitRawMetaPath) != 2 && len(splitRawMetaPath) != 3 {
return metaPath{}, fmt.Errorf("invalid metadata path '%s'", rawMetaPath)
}
suffix := splitRawMetaPath[len(splitRawMetaPath)-1]
if suffix != "json" {
return metaPath{}, fmt.Errorf("invalid metadata path (suffix) '%s'", rawMetaPath)
}
rawRole := splitRawMetaPath[len(splitRawMetaPath)-2]
if rawRole == "" {
return metaPath{}, fmt.Errorf("invalid metadata path (role) '%s'", rawMetaPath)
}
if len(splitRawMetaPath) == 2 {
return metaPath{
role: role(rawRole),
}, nil
}
rawVersion, err := strconv.ParseInt(splitRawMetaPath[0], 10, 64)
if err != nil {
return metaPath{}, fmt.Errorf("invalid metadata path (version) '%s': %w", rawMetaPath, err)
}
return metaPath{
role: role(rawRole),
version: rawVersion,
versionSet: true,
}, nil
}
func validateTargetFileHash(targetMeta data.TargetFileMeta, targetFile []byte) error {
if len(targetMeta.HashAlgorithms()) == 0 {
return fmt.Errorf("target file has no hash")
}
generatedMeta, err := util.GenerateFileMeta(bytes.NewBuffer(targetFile), targetMeta.HashAlgorithms()...)
if err != nil {
return err
}
err = util.FileMetaEqual(targetMeta.FileMeta, generatedMeta)
if err != nil {
return err
}
return nil
}
func unsafeUnmarshalRoot(raw []byte) (*data.Root, error) {
var signedRoot data.Signed
err := json.Unmarshal(raw, &signedRoot)
if err != nil {
return nil, err
}
var root data.Root
err = json.Unmarshal(signedRoot.Signed, &root)
if err != nil {
return nil, err
}
return &root, err
}
func unsafeUnmarshalTargets(raw []byte) (*data.Targets, error) {
var signedTargets data.Signed
err := json.Unmarshal(raw, &signedTargets)
if err != nil {
return nil, err
}
var targets data.Targets
err = json.Unmarshal(signedTargets.Signed, &targets)
if err != nil {
return nil, err
}
return &targets, err
}
func extractRootVersion(raw []byte) (int64, error) {
root, err := unsafeUnmarshalRoot(raw)
if err != nil {
return 0, err
}
return root.Version, nil
}

View File

@@ -1,6 +1,4 @@
The MIT License (MIT)
Copyright (c) 2015 go-logfmt
Copyright (c) 2015 Datadog, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,4 @@
## Overview
Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags
and histograms.

View File

@@ -0,0 +1,283 @@
package statsd
import (
"strings"
"sync"
"sync/atomic"
"time"
)
type (
countsMap map[string]*countMetric
gaugesMap map[string]*gaugeMetric
setsMap map[string]*setMetric
bufferedMetricMap map[string]*bufferedMetric
)
type aggregator struct {
nbContextGauge int32
nbContextCount int32
nbContextSet int32
countsM sync.RWMutex
gaugesM sync.RWMutex
setsM sync.RWMutex
gauges gaugesMap
counts countsMap
sets setsMap
histograms bufferedMetricContexts
distributions bufferedMetricContexts
timings bufferedMetricContexts
closed chan struct{}
client *Client
// aggregator implements ChannelMode mechanism to receive histograms,
// distributions and timings. Since they need sampling they need to
// lock for random. When using both ChannelMode and ExtendedAggregation
// we don't want goroutine to fight over the lock.
inputMetrics chan metric
stopChannelMode chan struct{}
wg sync.WaitGroup
}
type aggregatorMetrics struct {
nbContext int32
nbContextGauge int32
nbContextCount int32
nbContextSet int32
nbContextHistogram int32
nbContextDistribution int32
nbContextTiming int32
}
func newAggregator(c *Client) *aggregator {
return &aggregator{
client: c,
counts: countsMap{},
gauges: gaugesMap{},
sets: setsMap{},
histograms: newBufferedContexts(newHistogramMetric),
distributions: newBufferedContexts(newDistributionMetric),
timings: newBufferedContexts(newTimingMetric),
closed: make(chan struct{}),
stopChannelMode: make(chan struct{}),
}
}
func (a *aggregator) start(flushInterval time.Duration) {
ticker := time.NewTicker(flushInterval)
go func() {
for {
select {
case <-ticker.C:
a.flush()
case <-a.closed:
return
}
}
}()
}
func (a *aggregator) startReceivingMetric(bufferSize int, nbWorkers int) {
a.inputMetrics = make(chan metric, bufferSize)
for i := 0; i < nbWorkers; i++ {
a.wg.Add(1)
go a.pullMetric()
}
}
func (a *aggregator) stopReceivingMetric() {
close(a.stopChannelMode)
a.wg.Wait()
}
func (a *aggregator) stop() {
a.closed <- struct{}{}
}
func (a *aggregator) pullMetric() {
for {
select {
case m := <-a.inputMetrics:
switch m.metricType {
case histogram:
a.histogram(m.name, m.fvalue, m.tags, m.rate)
case distribution:
a.distribution(m.name, m.fvalue, m.tags, m.rate)
case timing:
a.timing(m.name, m.fvalue, m.tags, m.rate)
}
case <-a.stopChannelMode:
a.wg.Done()
return
}
}
}
func (a *aggregator) flush() {
for _, m := range a.flushMetrics() {
a.client.sendBlocking(m)
}
}
func (a *aggregator) flushTelemetryMetrics() *aggregatorMetrics {
if a == nil {
return nil
}
am := &aggregatorMetrics{
nbContextGauge: atomic.SwapInt32(&a.nbContextGauge, 0),
nbContextCount: atomic.SwapInt32(&a.nbContextCount, 0),
nbContextSet: atomic.SwapInt32(&a.nbContextSet, 0),
nbContextHistogram: a.histograms.resetAndGetNbContext(),
nbContextDistribution: a.distributions.resetAndGetNbContext(),
nbContextTiming: a.timings.resetAndGetNbContext(),
}
am.nbContext = am.nbContextGauge + am.nbContextCount + am.nbContextSet + am.nbContextHistogram + am.nbContextDistribution + am.nbContextTiming
return am
}
func (a *aggregator) flushMetrics() []metric {
metrics := []metric{}
// We reset the values to avoid sending 'zero' values for metrics not
// sampled during this flush interval
a.setsM.Lock()
sets := a.sets
a.sets = setsMap{}
a.setsM.Unlock()
for _, s := range sets {
metrics = append(metrics, s.flushUnsafe()...)
}
a.gaugesM.Lock()
gauges := a.gauges
a.gauges = gaugesMap{}
a.gaugesM.Unlock()
for _, g := range gauges {
metrics = append(metrics, g.flushUnsafe())
}
a.countsM.Lock()
counts := a.counts
a.counts = countsMap{}
a.countsM.Unlock()
for _, c := range counts {
metrics = append(metrics, c.flushUnsafe())
}
metrics = a.histograms.flush(metrics)
metrics = a.distributions.flush(metrics)
metrics = a.timings.flush(metrics)
atomic.AddInt32(&a.nbContextCount, int32(len(counts)))
atomic.AddInt32(&a.nbContextGauge, int32(len(gauges)))
atomic.AddInt32(&a.nbContextSet, int32(len(sets)))
return metrics
}
func getContext(name string, tags []string) string {
return name + ":" + strings.Join(tags, tagSeparatorSymbol)
}
func getContextAndTags(name string, tags []string) (string, string) {
stringTags := strings.Join(tags, tagSeparatorSymbol)
return name + ":" + stringTags, stringTags
}
func (a *aggregator) count(name string, value int64, tags []string) error {
context := getContext(name, tags)
a.countsM.RLock()
if count, found := a.counts[context]; found {
count.sample(value)
a.countsM.RUnlock()
return nil
}
a.countsM.RUnlock()
a.countsM.Lock()
// Check if another goroutines hasn't created the value betwen the RUnlock and 'Lock'
if count, found := a.counts[context]; found {
count.sample(value)
a.countsM.Unlock()
return nil
}
a.counts[context] = newCountMetric(name, value, tags)
a.countsM.Unlock()
return nil
}
func (a *aggregator) gauge(name string, value float64, tags []string) error {
context := getContext(name, tags)
a.gaugesM.RLock()
if gauge, found := a.gauges[context]; found {
gauge.sample(value)
a.gaugesM.RUnlock()
return nil
}
a.gaugesM.RUnlock()
gauge := newGaugeMetric(name, value, tags)
a.gaugesM.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if gauge, found := a.gauges[context]; found {
gauge.sample(value)
a.gaugesM.Unlock()
return nil
}
a.gauges[context] = gauge
a.gaugesM.Unlock()
return nil
}
func (a *aggregator) set(name string, value string, tags []string) error {
context := getContext(name, tags)
a.setsM.RLock()
if set, found := a.sets[context]; found {
set.sample(value)
a.setsM.RUnlock()
return nil
}
a.setsM.RUnlock()
a.setsM.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if set, found := a.sets[context]; found {
set.sample(value)
a.setsM.Unlock()
return nil
}
a.sets[context] = newSetMetric(name, value, tags)
a.setsM.Unlock()
return nil
}
// Only histograms, distributions and timings are sampled with a rate since we
// only pack them in on message instead of aggregating them. Discarding the
// sample rate will have impacts on the CPU and memory usage of the Agent.
// type alias for Client.sendToAggregator
type bufferedMetricSampleFunc func(name string, value float64, tags []string, rate float64) error
func (a *aggregator) histogram(name string, value float64, tags []string, rate float64) error {
return a.histograms.sample(name, value, tags, rate)
}
func (a *aggregator) distribution(name string, value float64, tags []string, rate float64) error {
return a.distributions.sample(name, value, tags, rate)
}
func (a *aggregator) timing(name string, value float64, tags []string, rate float64) error {
return a.timings.sample(name, value, tags, rate)
}

190
vendor/github.com/DataDog/datadog-go/statsd/buffer.go generated vendored Normal file
View File

@@ -0,0 +1,190 @@
package statsd
import (
"strconv"
)
type bufferFullError string
func (e bufferFullError) Error() string { return string(e) }
const errBufferFull = bufferFullError("statsd buffer is full")
type partialWriteError string
func (e partialWriteError) Error() string { return string(e) }
const errPartialWrite = partialWriteError("value partially written")
const metricOverhead = 512
// statsdBuffer is a buffer containing statsd messages
// this struct methods are NOT safe for concurent use
type statsdBuffer struct {
buffer []byte
maxSize int
maxElements int
elementCount int
}
func newStatsdBuffer(maxSize, maxElements int) *statsdBuffer {
return &statsdBuffer{
buffer: make([]byte, 0, maxSize+metricOverhead), // pre-allocate the needed size + metricOverhead to avoid having Go re-allocate on it's own if an element does not fit
maxSize: maxSize,
maxElements: maxElements,
}
}
func (b *statsdBuffer) writeGauge(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendGauge(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeCount(namespace string, globalTags []string, name string, value int64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendCount(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeHistogram(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendHistogram(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
// writeAggregated serialized as many values as possible in the current buffer and return the position in values where it stopped.
func (b *statsdBuffer) writeAggregated(metricSymbol []byte, namespace string, globalTags []string, name string, values []float64, tags string, tagSize int, precision int) (int, error) {
if b.elementCount >= b.maxElements {
return 0, errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendHeader(b.buffer, namespace, name)
// buffer already full
if len(b.buffer)+tagSize > b.maxSize {
b.buffer = originalBuffer
return 0, errBufferFull
}
// We add as many value as possible
var position int
for idx, v := range values {
previousBuffer := b.buffer
if idx != 0 {
b.buffer = append(b.buffer, ':')
}
b.buffer = strconv.AppendFloat(b.buffer, v, 'f', precision, 64)
// Should we stop serializing and switch to another buffer
if len(b.buffer)+tagSize > b.maxSize {
b.buffer = previousBuffer
break
}
position = idx + 1
}
// we could not add a single value
if position == 0 {
b.buffer = originalBuffer
return 0, errBufferFull
}
b.buffer = append(b.buffer, '|')
b.buffer = append(b.buffer, metricSymbol...)
b.buffer = appendTagsAggregated(b.buffer, globalTags, tags)
b.writeSeparator()
b.elementCount++
if position != len(values) {
return position, errPartialWrite
}
return position, nil
}
func (b *statsdBuffer) writeDistribution(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendDistribution(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeSet(namespace string, globalTags []string, name string, value string, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendSet(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeTiming(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendTiming(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeEvent(event Event, globalTags []string) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendEvent(b.buffer, event, globalTags)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeServiceCheck(serviceCheck ServiceCheck, globalTags []string) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendServiceCheck(b.buffer, serviceCheck, globalTags)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) validateNewElement(originalBuffer []byte) error {
if len(b.buffer) > b.maxSize {
b.buffer = originalBuffer
return errBufferFull
}
b.elementCount++
return nil
}
func (b *statsdBuffer) writeSeparator() {
b.buffer = append(b.buffer, '\n')
}
func (b *statsdBuffer) reset() {
b.buffer = b.buffer[:0]
b.elementCount = 0
}
func (b *statsdBuffer) bytes() []byte {
return b.buffer
}

View File

@@ -0,0 +1,40 @@
package statsd
type bufferPool struct {
pool chan *statsdBuffer
bufferMaxSize int
bufferMaxElements int
}
func newBufferPool(poolSize, bufferMaxSize, bufferMaxElements int) *bufferPool {
p := &bufferPool{
pool: make(chan *statsdBuffer, poolSize),
bufferMaxSize: bufferMaxSize,
bufferMaxElements: bufferMaxElements,
}
for i := 0; i < poolSize; i++ {
p.addNewBuffer()
}
return p
}
func (p *bufferPool) addNewBuffer() {
p.pool <- newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
}
func (p *bufferPool) borrowBuffer() *statsdBuffer {
select {
case b := <-p.pool:
return b
default:
return newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
}
}
func (p *bufferPool) returnBuffer(buffer *statsdBuffer) {
buffer.reset()
select {
case p.pool <- buffer:
default:
}
}

View File

@@ -0,0 +1,82 @@
package statsd
import (
"math/rand"
"sync"
"sync/atomic"
"time"
)
// bufferedMetricContexts represent the contexts for Histograms, Distributions
// and Timing. Since those 3 metric types behave the same way and are sampled
// with the same type they're represented by the same class.
type bufferedMetricContexts struct {
nbContext int32
mutex sync.RWMutex
values bufferedMetricMap
newMetric func(string, float64, string) *bufferedMetric
// Each bufferedMetricContexts uses its own random source and random
// lock to prevent goroutines from contending for the lock on the
// "math/rand" package-global random source (e.g. calls like
// "rand.Float64()" must acquire a shared lock to get the next
// pseudorandom number).
random *rand.Rand
randomLock sync.Mutex
}
func newBufferedContexts(newMetric func(string, float64, string) *bufferedMetric) bufferedMetricContexts {
return bufferedMetricContexts{
values: bufferedMetricMap{},
newMetric: newMetric,
// Note that calling "time.Now().UnixNano()" repeatedly quickly may return
// very similar values. That's fine for seeding the worker-specific random
// source because we just need an evenly distributed stream of float values.
// Do not use this random source for cryptographic randomness.
random: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
func (bc *bufferedMetricContexts) flush(metrics []metric) []metric {
bc.mutex.Lock()
values := bc.values
bc.values = bufferedMetricMap{}
bc.mutex.Unlock()
for _, d := range values {
metrics = append(metrics, d.flushUnsafe())
}
atomic.AddInt32(&bc.nbContext, int32(len(values)))
return metrics
}
func (bc *bufferedMetricContexts) sample(name string, value float64, tags []string, rate float64) error {
if !shouldSample(rate, bc.random, &bc.randomLock) {
return nil
}
context, stringTags := getContextAndTags(name, tags)
bc.mutex.RLock()
if v, found := bc.values[context]; found {
v.sample(value)
bc.mutex.RUnlock()
return nil
}
bc.mutex.RUnlock()
bc.mutex.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if v, found := bc.values[context]; found {
v.sample(value)
bc.mutex.Unlock()
return nil
}
bc.values[context] = bc.newMetric(name, value, stringTags)
bc.mutex.Unlock()
return nil
}
func (bc *bufferedMetricContexts) resetAndGetNbContext() int32 {
return atomic.SwapInt32(&bc.nbContext, 0)
}

91
vendor/github.com/DataDog/datadog-go/statsd/event.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
package statsd
import (
"fmt"
"time"
)
// Events support
// EventAlertType and EventAlertPriority became exported types after this issue was submitted: https://github.com/DataDog/datadog-go/issues/41
// The reason why they got exported is so that client code can directly use the types.
// EventAlertType is the alert type for events
type EventAlertType string
const (
// Info is the "info" AlertType for events
Info EventAlertType = "info"
// Error is the "error" AlertType for events
Error EventAlertType = "error"
// Warning is the "warning" AlertType for events
Warning EventAlertType = "warning"
// Success is the "success" AlertType for events
Success EventAlertType = "success"
)
// EventPriority is the event priority for events
type EventPriority string
const (
// Normal is the "normal" Priority for events
Normal EventPriority = "normal"
// Low is the "low" Priority for events
Low EventPriority = "low"
)
// An Event is an object that can be posted to your DataDog event stream.
type Event struct {
// Title of the event. Required.
Title string
// Text is the description of the event. Required.
Text string
// Timestamp is a timestamp for the event. If not provided, the dogstatsd
// server will set this to the current time.
Timestamp time.Time
// Hostname for the event.
Hostname string
// AggregationKey groups this event with others of the same key.
AggregationKey string
// Priority of the event. Can be statsd.Low or statsd.Normal.
Priority EventPriority
// SourceTypeName is a source type for the event.
SourceTypeName string
// AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success.
// If absent, the default value applied by the dogstatsd server is Info.
AlertType EventAlertType
// Tags for the event.
Tags []string
}
// NewEvent creates a new event with the given title and text. Error checking
// against these values is done at send-time, or upon running e.Check.
func NewEvent(title, text string) *Event {
return &Event{
Title: title,
Text: text,
}
}
// Check verifies that an event is valid.
func (e Event) Check() error {
if len(e.Title) == 0 {
return fmt.Errorf("statsd.Event title is required")
}
if len(e.Text) == 0 {
return fmt.Errorf("statsd.Event text is required")
}
return nil
}
// Encode returns the dogstatsd wire protocol representation for an event.
// Tags may be passed which will be added to the encoded output but not to
// the Event's list of tags, eg. for default tags.
func (e Event) Encode(tags ...string) (string, error) {
err := e.Check()
if err != nil {
return "", err
}
var buffer []byte
buffer = appendEvent(buffer, e, tags)
return string(buffer), nil
}

39
vendor/github.com/DataDog/datadog-go/statsd/fnv1a.go generated vendored Normal file
View File

@@ -0,0 +1,39 @@
package statsd
const (
// FNV-1a
offset32 = uint32(2166136261)
prime32 = uint32(16777619)
// init32 is what 32 bits hash values should be initialized with.
init32 = offset32
)
// HashString32 returns the hash of s.
func hashString32(s string) uint32 {
return addString32(init32, s)
}
// AddString32 adds the hash of s to the precomputed hash value h.
func addString32(h uint32, s string) uint32 {
i := 0
n := (len(s) / 8) * 8
for i != n {
h = (h ^ uint32(s[i])) * prime32
h = (h ^ uint32(s[i+1])) * prime32
h = (h ^ uint32(s[i+2])) * prime32
h = (h ^ uint32(s[i+3])) * prime32
h = (h ^ uint32(s[i+4])) * prime32
h = (h ^ uint32(s[i+5])) * prime32
h = (h ^ uint32(s[i+6])) * prime32
h = (h ^ uint32(s[i+7])) * prime32
i += 8
}
for _, c := range s[i:] {
h = (h ^ uint32(c)) * prime32
}
return h
}

257
vendor/github.com/DataDog/datadog-go/statsd/format.go generated vendored Normal file
View File

@@ -0,0 +1,257 @@
package statsd
import (
"strconv"
"strings"
)
var (
gaugeSymbol = []byte("g")
countSymbol = []byte("c")
histogramSymbol = []byte("h")
distributionSymbol = []byte("d")
setSymbol = []byte("s")
timingSymbol = []byte("ms")
tagSeparatorSymbol = ","
)
func appendHeader(buffer []byte, namespace string, name string) []byte {
if namespace != "" {
buffer = append(buffer, namespace...)
}
buffer = append(buffer, name...)
buffer = append(buffer, ':')
return buffer
}
func appendRate(buffer []byte, rate float64) []byte {
if rate < 1 {
buffer = append(buffer, "|@"...)
buffer = strconv.AppendFloat(buffer, rate, 'f', -1, 64)
}
return buffer
}
func appendWithoutNewlines(buffer []byte, s string) []byte {
// fastpath for strings without newlines
if strings.IndexByte(s, '\n') == -1 {
return append(buffer, s...)
}
for _, b := range []byte(s) {
if b != '\n' {
buffer = append(buffer, b)
}
}
return buffer
}
func appendTags(buffer []byte, globalTags []string, tags []string) []byte {
if len(globalTags) == 0 && len(tags) == 0 {
return buffer
}
buffer = append(buffer, "|#"...)
firstTag := true
for _, tag := range globalTags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
for _, tag := range tags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
return buffer
}
func appendTagsAggregated(buffer []byte, globalTags []string, tags string) []byte {
if len(globalTags) == 0 && tags == "" {
return buffer
}
buffer = append(buffer, "|#"...)
firstTag := true
for _, tag := range globalTags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
if tags != "" {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tags)
}
return buffer
}
func appendFloatMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, precision int) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = strconv.AppendFloat(buffer, value, 'f', precision, 64)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendIntegerMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = strconv.AppendInt(buffer, value, 10)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendStringMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = append(buffer, value...)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendGauge(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, gaugeSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendCount(buffer []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
return appendIntegerMetric(buffer, countSymbol, namespace, globalTags, name, value, tags, rate)
}
func appendHistogram(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, histogramSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendDistribution(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, distributionSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendSet(buffer []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
return appendStringMetric(buffer, setSymbol, namespace, globalTags, name, value, tags, rate)
}
func appendTiming(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, timingSymbol, namespace, globalTags, name, value, tags, rate, 6)
}
func escapedEventTextLen(text string) int {
return len(text) + strings.Count(text, "\n")
}
func appendEscapedEventText(buffer []byte, text string) []byte {
for _, b := range []byte(text) {
if b != '\n' {
buffer = append(buffer, b)
} else {
buffer = append(buffer, "\\n"...)
}
}
return buffer
}
func appendEvent(buffer []byte, event Event, globalTags []string) []byte {
escapedTextLen := escapedEventTextLen(event.Text)
buffer = append(buffer, "_e{"...)
buffer = strconv.AppendInt(buffer, int64(len(event.Title)), 10)
buffer = append(buffer, tagSeparatorSymbol...)
buffer = strconv.AppendInt(buffer, int64(escapedTextLen), 10)
buffer = append(buffer, "}:"...)
buffer = append(buffer, event.Title...)
buffer = append(buffer, '|')
if escapedTextLen != len(event.Text) {
buffer = appendEscapedEventText(buffer, event.Text)
} else {
buffer = append(buffer, event.Text...)
}
if !event.Timestamp.IsZero() {
buffer = append(buffer, "|d:"...)
buffer = strconv.AppendInt(buffer, int64(event.Timestamp.Unix()), 10)
}
if len(event.Hostname) != 0 {
buffer = append(buffer, "|h:"...)
buffer = append(buffer, event.Hostname...)
}
if len(event.AggregationKey) != 0 {
buffer = append(buffer, "|k:"...)
buffer = append(buffer, event.AggregationKey...)
}
if len(event.Priority) != 0 {
buffer = append(buffer, "|p:"...)
buffer = append(buffer, event.Priority...)
}
if len(event.SourceTypeName) != 0 {
buffer = append(buffer, "|s:"...)
buffer = append(buffer, event.SourceTypeName...)
}
if len(event.AlertType) != 0 {
buffer = append(buffer, "|t:"...)
buffer = append(buffer, string(event.AlertType)...)
}
buffer = appendTags(buffer, globalTags, event.Tags)
return buffer
}
func appendEscapedServiceCheckText(buffer []byte, text string) []byte {
for i := 0; i < len(text); i++ {
if text[i] == '\n' {
buffer = append(buffer, "\\n"...)
} else if text[i] == 'm' && i+1 < len(text) && text[i+1] == ':' {
buffer = append(buffer, "m\\:"...)
i++
} else {
buffer = append(buffer, text[i])
}
}
return buffer
}
func appendServiceCheck(buffer []byte, serviceCheck ServiceCheck, globalTags []string) []byte {
buffer = append(buffer, "_sc|"...)
buffer = append(buffer, serviceCheck.Name...)
buffer = append(buffer, '|')
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Status), 10)
if !serviceCheck.Timestamp.IsZero() {
buffer = append(buffer, "|d:"...)
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Timestamp.Unix()), 10)
}
if len(serviceCheck.Hostname) != 0 {
buffer = append(buffer, "|h:"...)
buffer = append(buffer, serviceCheck.Hostname...)
}
buffer = appendTags(buffer, globalTags, serviceCheck.Tags)
if len(serviceCheck.Message) != 0 {
buffer = append(buffer, "|m:"...)
buffer = appendEscapedServiceCheckText(buffer, serviceCheck.Message)
}
return buffer
}
func appendSeparator(buffer []byte) []byte {
return append(buffer, '\n')
}

181
vendor/github.com/DataDog/datadog-go/statsd/metrics.go generated vendored Normal file
View File

@@ -0,0 +1,181 @@
package statsd
import (
"math"
"sync"
"sync/atomic"
)
/*
Those are metrics type that can be aggregated on the client side:
- Gauge
- Count
- Set
*/
type countMetric struct {
value int64
name string
tags []string
}
func newCountMetric(name string, value int64, tags []string) *countMetric {
return &countMetric{
value: value,
name: name,
tags: tags,
}
}
func (c *countMetric) sample(v int64) {
atomic.AddInt64(&c.value, v)
}
func (c *countMetric) flushUnsafe() metric {
return metric{
metricType: count,
name: c.name,
tags: c.tags,
rate: 1,
ivalue: c.value,
}
}
// Gauge
type gaugeMetric struct {
value uint64
name string
tags []string
}
func newGaugeMetric(name string, value float64, tags []string) *gaugeMetric {
return &gaugeMetric{
value: math.Float64bits(value),
name: name,
tags: tags,
}
}
func (g *gaugeMetric) sample(v float64) {
atomic.StoreUint64(&g.value, math.Float64bits(v))
}
func (g *gaugeMetric) flushUnsafe() metric {
return metric{
metricType: gauge,
name: g.name,
tags: g.tags,
rate: 1,
fvalue: math.Float64frombits(g.value),
}
}
// Set
type setMetric struct {
data map[string]struct{}
name string
tags []string
sync.Mutex
}
func newSetMetric(name string, value string, tags []string) *setMetric {
set := &setMetric{
data: map[string]struct{}{},
name: name,
tags: tags,
}
set.data[value] = struct{}{}
return set
}
func (s *setMetric) sample(v string) {
s.Lock()
defer s.Unlock()
s.data[v] = struct{}{}
}
// Sets are aggregated on the agent side too. We flush the keys so a set from
// multiple application can be correctly aggregated on the agent side.
func (s *setMetric) flushUnsafe() []metric {
if len(s.data) == 0 {
return nil
}
metrics := make([]metric, len(s.data))
i := 0
for value := range s.data {
metrics[i] = metric{
metricType: set,
name: s.name,
tags: s.tags,
rate: 1,
svalue: value,
}
i++
}
return metrics
}
// Histograms, Distributions and Timings
type bufferedMetric struct {
sync.Mutex
data []float64
name string
// Histograms and Distributions store tags as one string since we need
// to compute its size multiple time when serializing.
tags string
mtype metricType
}
func (s *bufferedMetric) sample(v float64) {
s.Lock()
defer s.Unlock()
s.data = append(s.data, v)
}
func (s *bufferedMetric) flushUnsafe() metric {
return metric{
metricType: s.mtype,
name: s.name,
stags: s.tags,
rate: 1,
fvalues: s.data,
}
}
type histogramMetric = bufferedMetric
func newHistogramMetric(name string, value float64, stringTags string) *histogramMetric {
return &histogramMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: histogramAggregated,
}
}
type distributionMetric = bufferedMetric
func newDistributionMetric(name string, value float64, stringTags string) *distributionMetric {
return &distributionMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: distributionAggregated,
}
}
type timingMetric = bufferedMetric
func newTimingMetric(name string, value float64, stringTags string) *timingMetric {
return &timingMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: timingAggregated,
}
}

91
vendor/github.com/DataDog/datadog-go/statsd/noop.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
package statsd
import "time"
// NoOpClient is a statsd client that does nothing. Can be useful in testing
// situations for library users.
type NoOpClient struct{}
// Gauge does nothing and returns nil
func (n *NoOpClient) Gauge(name string, value float64, tags []string, rate float64) error {
return nil
}
// Count does nothing and returns nil
func (n *NoOpClient) Count(name string, value int64, tags []string, rate float64) error {
return nil
}
// Histogram does nothing and returns nil
func (n *NoOpClient) Histogram(name string, value float64, tags []string, rate float64) error {
return nil
}
// Distribution does nothing and returns nil
func (n *NoOpClient) Distribution(name string, value float64, tags []string, rate float64) error {
return nil
}
// Decr does nothing and returns nil
func (n *NoOpClient) Decr(name string, tags []string, rate float64) error {
return nil
}
// Incr does nothing and returns nil
func (n *NoOpClient) Incr(name string, tags []string, rate float64) error {
return nil
}
// Set does nothing and returns nil
func (n *NoOpClient) Set(name string, value string, tags []string, rate float64) error {
return nil
}
// Timing does nothing and returns nil
func (n *NoOpClient) Timing(name string, value time.Duration, tags []string, rate float64) error {
return nil
}
// TimeInMilliseconds does nothing and returns nil
func (n *NoOpClient) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
return nil
}
// Event does nothing and returns nil
func (n *NoOpClient) Event(e *Event) error {
return nil
}
// SimpleEvent does nothing and returns nil
func (n *NoOpClient) SimpleEvent(title, text string) error {
return nil
}
// ServiceCheck does nothing and returns nil
func (n *NoOpClient) ServiceCheck(sc *ServiceCheck) error {
return nil
}
// SimpleServiceCheck does nothing and returns nil
func (n *NoOpClient) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
return nil
}
// Close does nothing and returns nil
func (n *NoOpClient) Close() error {
return nil
}
// Flush does nothing and returns nil
func (n *NoOpClient) Flush() error {
return nil
}
// SetWriteTimeout does nothing and returns nil
func (n *NoOpClient) SetWriteTimeout(d time.Duration) error {
return nil
}
// Verify that NoOpClient implements the ClientInterface.
// https://golang.org/doc/faq#guarantee_satisfies_interface
var _ ClientInterface = &NoOpClient{}

323
vendor/github.com/DataDog/datadog-go/statsd/options.go generated vendored Normal file
View File

@@ -0,0 +1,323 @@
package statsd
import (
"fmt"
"math"
"strings"
"time"
)
var (
// DefaultNamespace is the default value for the Namespace option
DefaultNamespace = ""
// DefaultTags is the default value for the Tags option
DefaultTags = []string{}
// DefaultMaxBytesPerPayload is the default value for the MaxBytesPerPayload option
DefaultMaxBytesPerPayload = 0
// DefaultMaxMessagesPerPayload is the default value for the MaxMessagesPerPayload option
DefaultMaxMessagesPerPayload = math.MaxInt32
// DefaultBufferPoolSize is the default value for the DefaultBufferPoolSize option
DefaultBufferPoolSize = 0
// DefaultBufferFlushInterval is the default value for the BufferFlushInterval option
DefaultBufferFlushInterval = 100 * time.Millisecond
// DefaultBufferShardCount is the default value for the BufferShardCount option
DefaultBufferShardCount = 32
// DefaultSenderQueueSize is the default value for the DefaultSenderQueueSize option
DefaultSenderQueueSize = 0
// DefaultWriteTimeoutUDS is the default value for the WriteTimeoutUDS option
DefaultWriteTimeoutUDS = 100 * time.Millisecond
// DefaultTelemetry is the default value for the Telemetry option
DefaultTelemetry = true
// DefaultReceivingMode is the default behavior when sending metrics
DefaultReceivingMode = MutexMode
// DefaultChannelModeBufferSize is the default size of the channel holding incoming metrics
DefaultChannelModeBufferSize = 4096
// DefaultAggregationFlushInterval is the default interval for the aggregator to flush metrics.
// This should divide the Agent reporting period (default=10s) evenly to reduce "aliasing" that
// can cause values to appear irregular.
DefaultAggregationFlushInterval = 2 * time.Second
// DefaultAggregation
DefaultAggregation = false
// DefaultExtendedAggregation
DefaultExtendedAggregation = false
// DefaultDevMode
DefaultDevMode = false
)
// Options contains the configuration options for a client.
type Options struct {
// Namespace to prepend to all metrics, events and service checks name.
Namespace string
// Tags are global tags to be applied to every metrics, events and service checks.
Tags []string
// MaxBytesPerPayload is the maximum number of bytes a single payload will contain.
// The magic value 0 will set the option to the optimal size for the transport
// protocol used when creating the client: 1432 for UDP and 8192 for UDS.
MaxBytesPerPayload int
// MaxMessagesPerPayload is the maximum number of metrics, events and/or service checks a single payload will contain.
// This option can be set to `1` to create an unbuffered client.
MaxMessagesPerPayload int
// BufferPoolSize is the size of the pool of buffers in number of buffers.
// The magic value 0 will set the option to the optimal size for the transport
// protocol used when creating the client: 2048 for UDP and 512 for UDS.
BufferPoolSize int
// BufferFlushInterval is the interval after which the current buffer will get flushed.
BufferFlushInterval time.Duration
// BufferShardCount is the number of buffer "shards" that will be used.
// Those shards allows the use of multiple buffers at the same time to reduce
// lock contention.
BufferShardCount int
// SenderQueueSize is the size of the sender queue in number of buffers.
// The magic value 0 will set the option to the optimal size for the transport
// protocol used when creating the client: 2048 for UDP and 512 for UDS.
SenderQueueSize int
// WriteTimeoutUDS is the timeout after which a UDS packet is dropped.
WriteTimeoutUDS time.Duration
// Telemetry is a set of metrics automatically injected by the client in the
// dogstatsd stream to be able to monitor the client itself.
Telemetry bool
// ReceiveMode determins the behavior of the client when receiving to many
// metrics. The client will either drop the metrics if its buffers are
// full (ChannelMode mode) or block the caller until the metric can be
// handled (MutexMode mode). By default the client will MutexMode. This
// option should be set to ChannelMode only when use under very high
// load.
//
// MutexMode uses a mutex internally which is much faster than
// channel but causes some lock contention when used with a high number
// of threads. Mutex are sharded based on the metrics name which
// limit mutex contention when goroutines send different metrics.
//
// ChannelMode: uses channel (of ChannelModeBufferSize size) to send
// metrics and drop metrics if the channel is full. Sending metrics in
// this mode is slower that MutexMode (because of the channel), but
// will not block the application. This mode is made for application
// using many goroutines, sending the same metrics at a very high
// volume. The goal is to not slow down the application at the cost of
// dropping metrics and having a lower max throughput.
ReceiveMode ReceivingMode
// ChannelModeBufferSize is the size of the channel holding incoming metrics
ChannelModeBufferSize int
// AggregationFlushInterval is the interval for the aggregator to flush metrics
AggregationFlushInterval time.Duration
// [beta] Aggregation enables/disables client side aggregation for
// Gauges, Counts and Sets (compatible with every Agent's version).
Aggregation bool
// [beta] Extended aggregation enables/disables client side aggregation
// for all types. This feature is only compatible with Agent's versions
// >=7.25.0 or Agent's version >=6.25.0 && < 7.0.0.
ExtendedAggregation bool
// TelemetryAddr specify a different endpoint for telemetry metrics.
TelemetryAddr string
// DevMode enables the "dev" mode where the client sends much more
// telemetry metrics to help troubleshooting the client behavior.
DevMode bool
}
func resolveOptions(options []Option) (*Options, error) {
o := &Options{
Namespace: DefaultNamespace,
Tags: DefaultTags,
MaxBytesPerPayload: DefaultMaxBytesPerPayload,
MaxMessagesPerPayload: DefaultMaxMessagesPerPayload,
BufferPoolSize: DefaultBufferPoolSize,
BufferFlushInterval: DefaultBufferFlushInterval,
BufferShardCount: DefaultBufferShardCount,
SenderQueueSize: DefaultSenderQueueSize,
WriteTimeoutUDS: DefaultWriteTimeoutUDS,
Telemetry: DefaultTelemetry,
ReceiveMode: DefaultReceivingMode,
ChannelModeBufferSize: DefaultChannelModeBufferSize,
AggregationFlushInterval: DefaultAggregationFlushInterval,
Aggregation: DefaultAggregation,
ExtendedAggregation: DefaultExtendedAggregation,
DevMode: DefaultDevMode,
}
for _, option := range options {
err := option(o)
if err != nil {
return nil, err
}
}
return o, nil
}
// Option is a client option. Can return an error if validation fails.
type Option func(*Options) error
// WithNamespace sets the Namespace option.
func WithNamespace(namespace string) Option {
return func(o *Options) error {
if strings.HasSuffix(namespace, ".") {
o.Namespace = namespace
} else {
o.Namespace = namespace + "."
}
return nil
}
}
// WithTags sets the Tags option.
func WithTags(tags []string) Option {
return func(o *Options) error {
o.Tags = tags
return nil
}
}
// WithMaxMessagesPerPayload sets the MaxMessagesPerPayload option.
func WithMaxMessagesPerPayload(maxMessagesPerPayload int) Option {
return func(o *Options) error {
o.MaxMessagesPerPayload = maxMessagesPerPayload
return nil
}
}
// WithMaxBytesPerPayload sets the MaxBytesPerPayload option.
func WithMaxBytesPerPayload(MaxBytesPerPayload int) Option {
return func(o *Options) error {
o.MaxBytesPerPayload = MaxBytesPerPayload
return nil
}
}
// WithBufferPoolSize sets the BufferPoolSize option.
func WithBufferPoolSize(bufferPoolSize int) Option {
return func(o *Options) error {
o.BufferPoolSize = bufferPoolSize
return nil
}
}
// WithBufferFlushInterval sets the BufferFlushInterval option.
func WithBufferFlushInterval(bufferFlushInterval time.Duration) Option {
return func(o *Options) error {
o.BufferFlushInterval = bufferFlushInterval
return nil
}
}
// WithBufferShardCount sets the BufferShardCount option.
func WithBufferShardCount(bufferShardCount int) Option {
return func(o *Options) error {
if bufferShardCount < 1 {
return fmt.Errorf("BufferShardCount must be a positive integer")
}
o.BufferShardCount = bufferShardCount
return nil
}
}
// WithSenderQueueSize sets the SenderQueueSize option.
func WithSenderQueueSize(senderQueueSize int) Option {
return func(o *Options) error {
o.SenderQueueSize = senderQueueSize
return nil
}
}
// WithWriteTimeoutUDS sets the WriteTimeoutUDS option.
func WithWriteTimeoutUDS(writeTimeoutUDS time.Duration) Option {
return func(o *Options) error {
o.WriteTimeoutUDS = writeTimeoutUDS
return nil
}
}
// WithoutTelemetry disables the telemetry
func WithoutTelemetry() Option {
return func(o *Options) error {
o.Telemetry = false
return nil
}
}
// WithChannelMode will use channel to receive metrics
func WithChannelMode() Option {
return func(o *Options) error {
o.ReceiveMode = ChannelMode
return nil
}
}
// WithMutexMode will use mutex to receive metrics
func WithMutexMode() Option {
return func(o *Options) error {
o.ReceiveMode = MutexMode
return nil
}
}
// WithChannelModeBufferSize the channel buffer size when using "drop mode"
func WithChannelModeBufferSize(bufferSize int) Option {
return func(o *Options) error {
o.ChannelModeBufferSize = bufferSize
return nil
}
}
// WithAggregationInterval set the aggregation interval
func WithAggregationInterval(interval time.Duration) Option {
return func(o *Options) error {
o.AggregationFlushInterval = interval
return nil
}
}
// WithClientSideAggregation enables client side aggregation for Gauges, Counts
// and Sets. Client side aggregation is a beta feature.
func WithClientSideAggregation() Option {
return func(o *Options) error {
o.Aggregation = true
return nil
}
}
// WithoutClientSideAggregation disables client side aggregation.
func WithoutClientSideAggregation() Option {
return func(o *Options) error {
o.Aggregation = false
o.ExtendedAggregation = false
return nil
}
}
// WithExtendedClientSideAggregation enables client side aggregation for all
// types. This feature is only compatible with Agent's version >=6.25.0 &&
// <7.0.0 or Agent's versions >=7.25.0. Client side aggregation is a beta
// feature.
func WithExtendedClientSideAggregation() Option {
return func(o *Options) error {
o.Aggregation = true
o.ExtendedAggregation = true
return nil
}
}
// WithTelemetryAddr specify a different address for telemetry metrics.
func WithTelemetryAddr(addr string) Option {
return func(o *Options) error {
o.TelemetryAddr = addr
return nil
}
}
// WithDevMode enables client "dev" mode, sending more Telemetry metrics to
// help troubleshoot client behavior.
func WithDevMode() Option {
return func(o *Options) error {
o.DevMode = true
return nil
}
}
// WithoutDevMode disables client "dev" mode, sending more Telemetry metrics to
// help troubleshoot client behavior.
func WithoutDevMode() Option {
return func(o *Options) error {
o.DevMode = false
return nil
}
}

9
vendor/github.com/DataDog/datadog-go/statsd/pipe.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// +build !windows
package statsd
import "errors"
func newWindowsPipeWriter(pipepath string) (statsdWriter, error) {
return nil, errors.New("Windows Named Pipes are only supported on Windows")
}

View File

@@ -0,0 +1,84 @@
// +build windows
package statsd
import (
"net"
"sync"
"time"
"github.com/Microsoft/go-winio"
)
const defaultPipeTimeout = 1 * time.Millisecond
type pipeWriter struct {
mu sync.RWMutex
conn net.Conn
timeout time.Duration
pipepath string
}
func (p *pipeWriter) SetWriteTimeout(d time.Duration) error {
p.mu.Lock()
p.timeout = d
p.mu.Unlock()
return nil
}
func (p *pipeWriter) Write(data []byte) (n int, err error) {
conn, err := p.ensureConnection()
if err != nil {
return 0, err
}
p.mu.RLock()
conn.SetWriteDeadline(time.Now().Add(p.timeout))
p.mu.RUnlock()
n, err = conn.Write(data)
if err != nil {
if e, ok := err.(net.Error); !ok || !e.Temporary() {
// disconnected; retry again on next attempt
p.mu.Lock()
p.conn = nil
p.mu.Unlock()
}
}
return n, err
}
func (p *pipeWriter) ensureConnection() (net.Conn, error) {
p.mu.RLock()
conn := p.conn
p.mu.RUnlock()
if conn != nil {
return conn, nil
}
// looks like we might need to connect - try again with write locking.
p.mu.Lock()
defer p.mu.Unlock()
if p.conn != nil {
return p.conn, nil
}
newconn, err := winio.DialPipe(p.pipepath, nil)
if err != nil {
return nil, err
}
p.conn = newconn
return newconn, nil
}
func (p *pipeWriter) Close() error {
return p.conn.Close()
}
func newWindowsPipeWriter(pipepath string) (*pipeWriter, error) {
// Defer connection establishment to first write
return &pipeWriter{
conn: nil,
timeout: defaultPipeTimeout,
pipepath: pipepath,
}, nil
}

130
vendor/github.com/DataDog/datadog-go/statsd/sender.go generated vendored Normal file
View File

@@ -0,0 +1,130 @@
package statsd
import (
"sync/atomic"
"time"
)
// A statsdWriter offers a standard interface regardless of the underlying
// protocol. For now UDS and UPD writers are available.
// Attention: the underlying buffer of `data` is reused after a `statsdWriter.Write` call.
// `statsdWriter.Write` must be synchronous.
type statsdWriter interface {
Write(data []byte) (n int, err error)
SetWriteTimeout(time.Duration) error
Close() error
}
// SenderMetrics contains metrics about the health of the sender
type SenderMetrics struct {
TotalSentBytes uint64
TotalSentPayloads uint64
TotalDroppedPayloads uint64
TotalDroppedBytes uint64
TotalDroppedPayloadsQueueFull uint64
TotalDroppedBytesQueueFull uint64
TotalDroppedPayloadsWriter uint64
TotalDroppedBytesWriter uint64
}
type sender struct {
transport statsdWriter
pool *bufferPool
queue chan *statsdBuffer
metrics *SenderMetrics
stop chan struct{}
flushSignal chan struct{}
}
func newSender(transport statsdWriter, queueSize int, pool *bufferPool) *sender {
sender := &sender{
transport: transport,
pool: pool,
queue: make(chan *statsdBuffer, queueSize),
metrics: &SenderMetrics{},
stop: make(chan struct{}),
flushSignal: make(chan struct{}),
}
go sender.sendLoop()
return sender
}
func (s *sender) send(buffer *statsdBuffer) {
select {
case s.queue <- buffer:
default:
atomic.AddUint64(&s.metrics.TotalDroppedPayloads, 1)
atomic.AddUint64(&s.metrics.TotalDroppedBytes, uint64(len(buffer.bytes())))
atomic.AddUint64(&s.metrics.TotalDroppedPayloadsQueueFull, 1)
atomic.AddUint64(&s.metrics.TotalDroppedBytesQueueFull, uint64(len(buffer.bytes())))
s.pool.returnBuffer(buffer)
}
}
func (s *sender) write(buffer *statsdBuffer) {
_, err := s.transport.Write(buffer.bytes())
if err != nil {
atomic.AddUint64(&s.metrics.TotalDroppedPayloads, 1)
atomic.AddUint64(&s.metrics.TotalDroppedBytes, uint64(len(buffer.bytes())))
atomic.AddUint64(&s.metrics.TotalDroppedPayloadsWriter, 1)
atomic.AddUint64(&s.metrics.TotalDroppedBytesWriter, uint64(len(buffer.bytes())))
} else {
atomic.AddUint64(&s.metrics.TotalSentPayloads, 1)
atomic.AddUint64(&s.metrics.TotalSentBytes, uint64(len(buffer.bytes())))
}
s.pool.returnBuffer(buffer)
}
func (s *sender) flushTelemetryMetrics() SenderMetrics {
return SenderMetrics{
TotalSentBytes: atomic.SwapUint64(&s.metrics.TotalSentBytes, 0),
TotalSentPayloads: atomic.SwapUint64(&s.metrics.TotalSentPayloads, 0),
TotalDroppedPayloads: atomic.SwapUint64(&s.metrics.TotalDroppedPayloads, 0),
TotalDroppedBytes: atomic.SwapUint64(&s.metrics.TotalDroppedBytes, 0),
TotalDroppedPayloadsQueueFull: atomic.SwapUint64(&s.metrics.TotalDroppedPayloadsQueueFull, 0),
TotalDroppedBytesQueueFull: atomic.SwapUint64(&s.metrics.TotalDroppedBytesQueueFull, 0),
TotalDroppedPayloadsWriter: atomic.SwapUint64(&s.metrics.TotalDroppedPayloadsWriter, 0),
TotalDroppedBytesWriter: atomic.SwapUint64(&s.metrics.TotalDroppedBytesWriter, 0),
}
}
func (s *sender) sendLoop() {
defer close(s.stop)
for {
select {
case buffer := <-s.queue:
s.write(buffer)
case <-s.stop:
return
case <-s.flushSignal:
// At that point we know that the workers are paused (the statsd client
// will pause them before calling sender.flush()).
// So we can fully flush the input queue
s.flushInputQueue()
s.flushSignal <- struct{}{}
}
}
}
func (s *sender) flushInputQueue() {
for {
select {
case buffer := <-s.queue:
s.write(buffer)
default:
return
}
}
}
func (s *sender) flush() {
s.flushSignal <- struct{}{}
<-s.flushSignal
}
func (s *sender) close() error {
s.stop <- struct{}{}
<-s.stop
s.flushInputQueue()
return s.transport.Close()
}

View File

@@ -0,0 +1,70 @@
package statsd
import (
"fmt"
"time"
)
// ServiceCheckStatus support
type ServiceCheckStatus byte
const (
// Ok is the "ok" ServiceCheck status
Ok ServiceCheckStatus = 0
// Warn is the "warning" ServiceCheck status
Warn ServiceCheckStatus = 1
// Critical is the "critical" ServiceCheck status
Critical ServiceCheckStatus = 2
// Unknown is the "unknown" ServiceCheck status
Unknown ServiceCheckStatus = 3
)
// A ServiceCheck is an object that contains status of DataDog service check.
type ServiceCheck struct {
// Name of the service check. Required.
Name string
// Status of service check. Required.
Status ServiceCheckStatus
// Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd
// server will set this to the current time.
Timestamp time.Time
// Hostname for the serviceCheck.
Hostname string
// A message describing the current state of the serviceCheck.
Message string
// Tags for the serviceCheck.
Tags []string
}
// NewServiceCheck creates a new serviceCheck with the given name and status. Error checking
// against these values is done at send-time, or upon running sc.Check.
func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck {
return &ServiceCheck{
Name: name,
Status: status,
}
}
// Check verifies that a service check is valid.
func (sc ServiceCheck) Check() error {
if len(sc.Name) == 0 {
return fmt.Errorf("statsd.ServiceCheck name is required")
}
if byte(sc.Status) < 0 || byte(sc.Status) > 3 {
return fmt.Errorf("statsd.ServiceCheck status has invalid value")
}
return nil
}
// Encode returns the dogstatsd wire protocol representation for a service check.
// Tags may be passed which will be added to the encoded output but not to
// the Service Check's list of tags, eg. for default tags.
func (sc ServiceCheck) Encode(tags ...string) (string, error) {
err := sc.Check()
if err != nil {
return "", err
}
var buffer []byte
buffer = appendServiceCheck(buffer, sc, tags)
return string(buffer), nil
}

687
vendor/github.com/DataDog/datadog-go/statsd/statsd.go generated vendored Normal file
View File

@@ -0,0 +1,687 @@
// Copyright 2013 Ooyala, Inc.
/*
Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd,
adding tags and histograms and pushing upstream to Datadog.
Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD.
statsd is based on go-statsd-client.
*/
package statsd
import (
"errors"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
/*
OptimalUDPPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes
is optimal for regular networks with an MTU of 1500 so datagrams don't get
fragmented. It's generally recommended not to fragment UDP datagrams as losing
a single fragment will cause the entire datagram to be lost.
*/
const OptimalUDPPayloadSize = 1432
/*
MaxUDPPayloadSize defines the maximum payload size for a UDP datagram.
Its value comes from the calculation: 65535 bytes Max UDP datagram size -
8byte UDP header - 60byte max IP headers
any number greater than that will see frames being cut out.
*/
const MaxUDPPayloadSize = 65467
// DefaultUDPBufferPoolSize is the default size of the buffer pool for UDP clients.
const DefaultUDPBufferPoolSize = 2048
// DefaultUDSBufferPoolSize is the default size of the buffer pool for UDS clients.
const DefaultUDSBufferPoolSize = 512
/*
DefaultMaxAgentPayloadSize is the default maximum payload size the agent
can receive. This can be adjusted by changing dogstatsd_buffer_size in the
agent configuration file datadog.yaml. This is also used as the optimal payload size
for UDS datagrams.
*/
const DefaultMaxAgentPayloadSize = 8192
/*
UnixAddressPrefix holds the prefix to use to enable Unix Domain Socket
traffic instead of UDP.
*/
const UnixAddressPrefix = "unix://"
/*
WindowsPipeAddressPrefix holds the prefix to use to enable Windows Named Pipes
traffic instead of UDP.
*/
const WindowsPipeAddressPrefix = `\\.\pipe\`
const (
agentHostEnvVarName = "DD_AGENT_HOST"
agentPortEnvVarName = "DD_DOGSTATSD_PORT"
defaultUDPPort = "8125"
)
/*
ddEnvTagsMapping is a mapping of each "DD_" prefixed environment variable
to a specific tag name. We use a slice to keep the order and simplify tests.
*/
var ddEnvTagsMapping = []struct{ envName, tagName string }{
{"DD_ENTITY_ID", "dd.internal.entity_id"}, // Client-side entity ID injection for container tagging.
{"DD_ENV", "env"}, // The name of the env in which the service runs.
{"DD_SERVICE", "service"}, // The name of the running service.
{"DD_VERSION", "version"}, // The current version of the running service.
}
type metricType int
const (
gauge metricType = iota
count
histogram
histogramAggregated
distribution
distributionAggregated
set
timing
timingAggregated
event
serviceCheck
)
type ReceivingMode int
const (
MutexMode ReceivingMode = iota
ChannelMode
)
const (
WriterNameUDP string = "udp"
WriterNameUDS string = "uds"
WriterWindowsPipe string = "pipe"
)
type metric struct {
metricType metricType
namespace string
globalTags []string
name string
fvalue float64
fvalues []float64
ivalue int64
svalue string
evalue *Event
scvalue *ServiceCheck
tags []string
stags string
rate float64
}
type noClientErr string
// ErrNoClient is returned if statsd reporting methods are invoked on
// a nil client.
const ErrNoClient = noClientErr("statsd client is nil")
func (e noClientErr) Error() string {
return string(e)
}
// ClientInterface is an interface that exposes the common client functions for the
// purpose of being able to provide a no-op client or even mocking. This can aid
// downstream users' with their testing.
type ClientInterface interface {
// Gauge measures the value of a metric at a particular time.
Gauge(name string, value float64, tags []string, rate float64) error
// Count tracks how many times something happened per second.
Count(name string, value int64, tags []string, rate float64) error
// Histogram tracks the statistical distribution of a set of values on each host.
Histogram(name string, value float64, tags []string, rate float64) error
// Distribution tracks the statistical distribution of a set of values across your infrastructure.
Distribution(name string, value float64, tags []string, rate float64) error
// Decr is just Count of -1
Decr(name string, tags []string, rate float64) error
// Incr is just Count of 1
Incr(name string, tags []string, rate float64) error
// Set counts the number of unique elements in a group.
Set(name string, value string, tags []string, rate float64) error
// Timing sends timing information, it is an alias for TimeInMilliseconds
Timing(name string, value time.Duration, tags []string, rate float64) error
// TimeInMilliseconds sends timing information in milliseconds.
// It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
TimeInMilliseconds(name string, value float64, tags []string, rate float64) error
// Event sends the provided Event.
Event(e *Event) error
// SimpleEvent sends an event with the provided title and text.
SimpleEvent(title, text string) error
// ServiceCheck sends the provided ServiceCheck.
ServiceCheck(sc *ServiceCheck) error
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
SimpleServiceCheck(name string, status ServiceCheckStatus) error
// Close the client connection.
Close() error
// Flush forces a flush of all the queued dogstatsd payloads.
Flush() error
// SetWriteTimeout allows the user to set a custom write timeout.
SetWriteTimeout(d time.Duration) error
}
// A Client is a handle for sending messages to dogstatsd. It is safe to
// use one Client from multiple goroutines simultaneously.
type Client struct {
// Sender handles the underlying networking protocol
sender *sender
// Namespace to prepend to all statsd calls
Namespace string
// Tags are global tags to be added to every statsd call
Tags []string
// skipErrors turns off error passing and allows UDS to emulate UDP behaviour
SkipErrors bool
flushTime time.Duration
metrics *ClientMetrics
telemetry *telemetryClient
stop chan struct{}
wg sync.WaitGroup
workers []*worker
closerLock sync.Mutex
workersMode ReceivingMode
aggregatorMode ReceivingMode
agg *aggregator
aggExtended *aggregator
options []Option
addrOption string
}
// ClientMetrics contains metrics about the client
type ClientMetrics struct {
TotalMetrics uint64
TotalMetricsGauge uint64
TotalMetricsCount uint64
TotalMetricsHistogram uint64
TotalMetricsDistribution uint64
TotalMetricsSet uint64
TotalMetricsTiming uint64
TotalEvents uint64
TotalServiceChecks uint64
TotalDroppedOnReceive uint64
}
// Verify that Client implements the ClientInterface.
// https://golang.org/doc/faq#guarantee_satisfies_interface
var _ ClientInterface = &Client{}
func resolveAddr(addr string) string {
envPort := ""
if addr == "" {
addr = os.Getenv(agentHostEnvVarName)
envPort = os.Getenv(agentPortEnvVarName)
}
if addr == "" {
return ""
}
if !strings.HasPrefix(addr, WindowsPipeAddressPrefix) && !strings.HasPrefix(addr, UnixAddressPrefix) {
if !strings.Contains(addr, ":") {
if envPort != "" {
addr = fmt.Sprintf("%s:%s", addr, envPort)
} else {
addr = fmt.Sprintf("%s:%s", addr, defaultUDPPort)
}
}
}
return addr
}
func createWriter(addr string) (statsdWriter, string, error) {
addr = resolveAddr(addr)
if addr == "" {
return nil, "", errors.New("No address passed and autodetection from environment failed")
}
switch {
case strings.HasPrefix(addr, WindowsPipeAddressPrefix):
w, err := newWindowsPipeWriter(addr)
return w, WriterWindowsPipe, err
case strings.HasPrefix(addr, UnixAddressPrefix):
w, err := newUDSWriter(addr[len(UnixAddressPrefix):])
return w, WriterNameUDS, err
default:
w, err := newUDPWriter(addr)
return w, WriterNameUDP, err
}
}
// New returns a pointer to a new Client given an addr in the format "hostname:port" for UDP,
// "unix:///path/to/socket" for UDS or "\\.\pipe\path\to\pipe" for Windows Named Pipes.
func New(addr string, options ...Option) (*Client, error) {
o, err := resolveOptions(options)
if err != nil {
return nil, err
}
w, writerType, err := createWriter(addr)
if err != nil {
return nil, err
}
client, err := newWithWriter(w, o, writerType)
if err == nil {
client.options = append(client.options, options...)
client.addrOption = addr
}
return client, err
}
// NewWithWriter creates a new Client with given writer. Writer is a
// io.WriteCloser + SetWriteTimeout(time.Duration) error
func NewWithWriter(w statsdWriter, options ...Option) (*Client, error) {
o, err := resolveOptions(options)
if err != nil {
return nil, err
}
return newWithWriter(w, o, "custom")
}
// CloneWithExtraOptions create a new Client with extra options
func CloneWithExtraOptions(c *Client, options ...Option) (*Client, error) {
if c == nil {
return nil, ErrNoClient
}
if c.addrOption == "" {
return nil, fmt.Errorf("can't clone client with no addrOption")
}
opt := append(c.options, options...)
return New(c.addrOption, opt...)
}
func newWithWriter(w statsdWriter, o *Options, writerName string) (*Client, error) {
w.SetWriteTimeout(o.WriteTimeoutUDS)
c := Client{
Namespace: o.Namespace,
Tags: o.Tags,
metrics: &ClientMetrics{},
}
// Inject values of DD_* environment variables as global tags.
for _, mapping := range ddEnvTagsMapping {
if value := os.Getenv(mapping.envName); value != "" {
c.Tags = append(c.Tags, fmt.Sprintf("%s:%s", mapping.tagName, value))
}
}
if o.MaxBytesPerPayload == 0 {
if writerName == WriterNameUDS {
o.MaxBytesPerPayload = DefaultMaxAgentPayloadSize
} else {
o.MaxBytesPerPayload = OptimalUDPPayloadSize
}
}
if o.BufferPoolSize == 0 {
if writerName == WriterNameUDS {
o.BufferPoolSize = DefaultUDSBufferPoolSize
} else {
o.BufferPoolSize = DefaultUDPBufferPoolSize
}
}
if o.SenderQueueSize == 0 {
if writerName == WriterNameUDS {
o.SenderQueueSize = DefaultUDSBufferPoolSize
} else {
o.SenderQueueSize = DefaultUDPBufferPoolSize
}
}
bufferPool := newBufferPool(o.BufferPoolSize, o.MaxBytesPerPayload, o.MaxMessagesPerPayload)
c.sender = newSender(w, o.SenderQueueSize, bufferPool)
c.aggregatorMode = o.ReceiveMode
c.workersMode = o.ReceiveMode
// ChannelMode mode at the worker level is not enabled when
// ExtendedAggregation is since the user app will not directly
// use the worker (the aggregator sit between the app and the
// workers).
if o.ExtendedAggregation {
c.workersMode = MutexMode
}
if o.Aggregation || o.ExtendedAggregation {
c.agg = newAggregator(&c)
c.agg.start(o.AggregationFlushInterval)
if o.ExtendedAggregation {
c.aggExtended = c.agg
if c.aggregatorMode == ChannelMode {
c.agg.startReceivingMetric(o.ChannelModeBufferSize, o.BufferShardCount)
}
}
}
for i := 0; i < o.BufferShardCount; i++ {
w := newWorker(bufferPool, c.sender)
c.workers = append(c.workers, w)
if c.workersMode == ChannelMode {
w.startReceivingMetric(o.ChannelModeBufferSize)
}
}
c.flushTime = o.BufferFlushInterval
c.stop = make(chan struct{}, 1)
c.wg.Add(1)
go func() {
defer c.wg.Done()
c.watch()
}()
if o.Telemetry {
if o.TelemetryAddr == "" {
c.telemetry = newTelemetryClient(&c, writerName, o.DevMode)
} else {
var err error
c.telemetry, err = newTelemetryClientWithCustomAddr(&c, writerName, o.DevMode, o.TelemetryAddr, bufferPool)
if err != nil {
return nil, err
}
}
c.telemetry.run(&c.wg, c.stop)
}
return &c, nil
}
// NewBuffered returns a Client that buffers its output and sends it in chunks.
// Buflen is the length of the buffer in number of commands.
//
// When addr is empty, the client will default to a UDP client and use the DD_AGENT_HOST
// and (optionally) the DD_DOGSTATSD_PORT environment variables to build the target address.
func NewBuffered(addr string, buflen int) (*Client, error) {
return New(addr, WithMaxMessagesPerPayload(buflen))
}
// SetWriteTimeout allows the user to set a custom UDS write timeout. Not supported for UDP
// or Windows Pipes.
func (c *Client) SetWriteTimeout(d time.Duration) error {
if c == nil {
return ErrNoClient
}
return c.sender.transport.SetWriteTimeout(d)
}
func (c *Client) watch() {
ticker := time.NewTicker(c.flushTime)
for {
select {
case <-ticker.C:
for _, w := range c.workers {
w.flush()
}
case <-c.stop:
ticker.Stop()
return
}
}
}
// Flush forces a flush of all the queued dogstatsd payloads This method is
// blocking and will not return until everything is sent through the network.
// In MutexMode, this will also block sampling new data to the client while the
// workers and sender are flushed.
func (c *Client) Flush() error {
if c == nil {
return ErrNoClient
}
if c.agg != nil {
c.agg.flush()
}
for _, w := range c.workers {
w.pause()
defer w.unpause()
w.flushUnsafe()
}
// Now that the worker are pause the sender can flush the queue between
// worker and senders
c.sender.flush()
return nil
}
func (c *Client) FlushTelemetryMetrics() ClientMetrics {
cm := ClientMetrics{
TotalMetricsGauge: atomic.SwapUint64(&c.metrics.TotalMetricsGauge, 0),
TotalMetricsCount: atomic.SwapUint64(&c.metrics.TotalMetricsCount, 0),
TotalMetricsSet: atomic.SwapUint64(&c.metrics.TotalMetricsSet, 0),
TotalMetricsHistogram: atomic.SwapUint64(&c.metrics.TotalMetricsHistogram, 0),
TotalMetricsDistribution: atomic.SwapUint64(&c.metrics.TotalMetricsDistribution, 0),
TotalMetricsTiming: atomic.SwapUint64(&c.metrics.TotalMetricsTiming, 0),
TotalEvents: atomic.SwapUint64(&c.metrics.TotalEvents, 0),
TotalServiceChecks: atomic.SwapUint64(&c.metrics.TotalServiceChecks, 0),
TotalDroppedOnReceive: atomic.SwapUint64(&c.metrics.TotalDroppedOnReceive, 0),
}
cm.TotalMetrics = cm.TotalMetricsGauge + cm.TotalMetricsCount +
cm.TotalMetricsSet + cm.TotalMetricsHistogram +
cm.TotalMetricsDistribution + cm.TotalMetricsTiming
return cm
}
func (c *Client) send(m metric) error {
h := hashString32(m.name)
worker := c.workers[h%uint32(len(c.workers))]
if c.workersMode == ChannelMode {
select {
case worker.inputMetrics <- m:
default:
atomic.AddUint64(&c.metrics.TotalDroppedOnReceive, 1)
}
return nil
}
return worker.processMetric(m)
}
// sendBlocking is used by the aggregator to inject aggregated metrics.
func (c *Client) sendBlocking(m metric) error {
m.globalTags = c.Tags
m.namespace = c.Namespace
h := hashString32(m.name)
worker := c.workers[h%uint32(len(c.workers))]
return worker.processMetric(m)
}
func (c *Client) sendToAggregator(mType metricType, name string, value float64, tags []string, rate float64, f bufferedMetricSampleFunc) error {
if c.aggregatorMode == ChannelMode {
select {
case c.aggExtended.inputMetrics <- metric{metricType: mType, name: name, fvalue: value, tags: tags, rate: rate}:
default:
atomic.AddUint64(&c.metrics.TotalDroppedOnReceive, 1)
}
return nil
}
return f(name, value, tags, rate)
}
// Gauge measures the value of a metric at a particular time.
func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsGauge, 1)
if c.agg != nil {
return c.agg.gauge(name, value, tags)
}
return c.send(metric{metricType: gauge, name: name, fvalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Count tracks how many times something happened per second.
func (c *Client) Count(name string, value int64, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsCount, 1)
if c.agg != nil {
return c.agg.count(name, value, tags)
}
return c.send(metric{metricType: count, name: name, ivalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Histogram tracks the statistical distribution of a set of values on each host.
func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsHistogram, 1)
if c.aggExtended != nil {
return c.sendToAggregator(histogram, name, value, tags, rate, c.aggExtended.histogram)
}
return c.send(metric{metricType: histogram, name: name, fvalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Distribution tracks the statistical distribution of a set of values across your infrastructure.
func (c *Client) Distribution(name string, value float64, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsDistribution, 1)
if c.aggExtended != nil {
return c.sendToAggregator(distribution, name, value, tags, rate, c.aggExtended.distribution)
}
return c.send(metric{metricType: distribution, name: name, fvalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Decr is just Count of -1
func (c *Client) Decr(name string, tags []string, rate float64) error {
return c.Count(name, -1, tags, rate)
}
// Incr is just Count of 1
func (c *Client) Incr(name string, tags []string, rate float64) error {
return c.Count(name, 1, tags, rate)
}
// Set counts the number of unique elements in a group.
func (c *Client) Set(name string, value string, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsSet, 1)
if c.agg != nil {
return c.agg.set(name, value, tags)
}
return c.send(metric{metricType: set, name: name, svalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Timing sends timing information, it is an alias for TimeInMilliseconds
func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error {
return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate)
}
// TimeInMilliseconds sends timing information in milliseconds.
// It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalMetricsTiming, 1)
if c.aggExtended != nil {
return c.sendToAggregator(timing, name, value, tags, rate, c.aggExtended.timing)
}
return c.send(metric{metricType: timing, name: name, fvalue: value, tags: tags, rate: rate, globalTags: c.Tags, namespace: c.Namespace})
}
// Event sends the provided Event.
func (c *Client) Event(e *Event) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalEvents, 1)
return c.send(metric{metricType: event, evalue: e, rate: 1, globalTags: c.Tags, namespace: c.Namespace})
}
// SimpleEvent sends an event with the provided title and text.
func (c *Client) SimpleEvent(title, text string) error {
e := NewEvent(title, text)
return c.Event(e)
}
// ServiceCheck sends the provided ServiceCheck.
func (c *Client) ServiceCheck(sc *ServiceCheck) error {
if c == nil {
return ErrNoClient
}
atomic.AddUint64(&c.metrics.TotalServiceChecks, 1)
return c.send(metric{metricType: serviceCheck, scvalue: sc, rate: 1, globalTags: c.Tags, namespace: c.Namespace})
}
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
sc := NewServiceCheck(name, status)
return c.ServiceCheck(sc)
}
// Close the client connection.
func (c *Client) Close() error {
if c == nil {
return ErrNoClient
}
// Acquire closer lock to ensure only one thread can close the stop channel
c.closerLock.Lock()
defer c.closerLock.Unlock()
// Notify all other threads that they should stop
select {
case <-c.stop:
return nil
default:
}
close(c.stop)
if c.workersMode == ChannelMode {
for _, w := range c.workers {
w.stopReceivingMetric()
}
}
// flush the aggregator first
if c.agg != nil {
if c.aggExtended != nil && c.aggregatorMode == ChannelMode {
c.agg.stopReceivingMetric()
}
c.agg.stop()
}
// Wait for the threads to stop
c.wg.Wait()
c.Flush()
return c.sender.close()
}

View File

@@ -0,0 +1,151 @@
package statsd
import (
"fmt"
"sync"
"time"
)
/*
TelemetryInterval is the interval at which telemetry will be sent by the client.
*/
const TelemetryInterval = 10 * time.Second
/*
clientTelemetryTag is a tag identifying this specific client.
*/
var clientTelemetryTag = "client:go"
/*
clientVersionTelemetryTag is a tag identifying this specific client version.
*/
var clientVersionTelemetryTag = "client_version:4.8.2"
type telemetryClient struct {
c *Client
tags []string
tagsByType map[metricType][]string
sender *sender
worker *worker
devMode bool
}
func newTelemetryClient(c *Client, transport string, devMode bool) *telemetryClient {
t := &telemetryClient{
c: c,
tags: append(c.Tags, clientTelemetryTag, clientVersionTelemetryTag, "client_transport:"+transport),
tagsByType: map[metricType][]string{},
devMode: devMode,
}
if devMode {
t.tagsByType[gauge] = append(append([]string{}, t.tags...), "metrics_type:gauge")
t.tagsByType[count] = append(append([]string{}, t.tags...), "metrics_type:count")
t.tagsByType[set] = append(append([]string{}, t.tags...), "metrics_type:set")
t.tagsByType[timing] = append(append([]string{}, t.tags...), "metrics_type:timing")
t.tagsByType[histogram] = append(append([]string{}, t.tags...), "metrics_type:histogram")
t.tagsByType[distribution] = append(append([]string{}, t.tags...), "metrics_type:distribution")
t.tagsByType[timing] = append(append([]string{}, t.tags...), "metrics_type:timing")
}
return t
}
func newTelemetryClientWithCustomAddr(c *Client, transport string, devMode bool, telemetryAddr string, pool *bufferPool) (*telemetryClient, error) {
telemetryWriter, _, err := createWriter(telemetryAddr)
if err != nil {
return nil, fmt.Errorf("Could not resolve telemetry address: %v", err)
}
t := newTelemetryClient(c, transport, devMode)
// Creating a custom sender/worker with 1 worker in mutex mode for the
// telemetry that share the same bufferPool.
// FIXME due to performance pitfall, we're always using UDP defaults
// even for UDS.
t.sender = newSender(telemetryWriter, DefaultUDPBufferPoolSize, pool)
t.worker = newWorker(pool, t.sender)
return t, nil
}
func (t *telemetryClient) run(wg *sync.WaitGroup, stop chan struct{}) {
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(TelemetryInterval)
for {
select {
case <-ticker.C:
t.sendTelemetry()
case <-stop:
ticker.Stop()
if t.sender != nil {
t.sender.close()
}
return
}
}
}()
}
func (t *telemetryClient) sendTelemetry() {
for _, m := range t.flush() {
if t.worker != nil {
t.worker.processMetric(m)
} else {
t.c.send(m)
}
}
if t.worker != nil {
t.worker.flush()
}
}
// flushTelemetry returns Telemetry metrics to be flushed. It's its own function to ease testing.
func (t *telemetryClient) flush() []metric {
m := []metric{}
// same as Count but without global namespace
telemetryCount := func(name string, value int64, tags []string) {
m = append(m, metric{metricType: count, name: name, ivalue: value, tags: tags, rate: 1})
}
clientMetrics := t.c.FlushTelemetryMetrics()
telemetryCount("datadog.dogstatsd.client.metrics", int64(clientMetrics.TotalMetrics), t.tags)
if t.devMode {
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsGauge), t.tagsByType[gauge])
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsCount), t.tagsByType[count])
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsHistogram), t.tagsByType[histogram])
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsDistribution), t.tagsByType[distribution])
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsSet), t.tagsByType[set])
telemetryCount("datadog.dogstatsd.client.metrics_by_type", int64(clientMetrics.TotalMetricsTiming), t.tagsByType[timing])
}
telemetryCount("datadog.dogstatsd.client.events", int64(clientMetrics.TotalEvents), t.tags)
telemetryCount("datadog.dogstatsd.client.service_checks", int64(clientMetrics.TotalServiceChecks), t.tags)
telemetryCount("datadog.dogstatsd.client.metric_dropped_on_receive", int64(clientMetrics.TotalDroppedOnReceive), t.tags)
senderMetrics := t.c.sender.flushTelemetryMetrics()
telemetryCount("datadog.dogstatsd.client.packets_sent", int64(senderMetrics.TotalSentPayloads), t.tags)
telemetryCount("datadog.dogstatsd.client.bytes_sent", int64(senderMetrics.TotalSentBytes), t.tags)
telemetryCount("datadog.dogstatsd.client.packets_dropped", int64(senderMetrics.TotalDroppedPayloads), t.tags)
telemetryCount("datadog.dogstatsd.client.bytes_dropped", int64(senderMetrics.TotalDroppedBytes), t.tags)
telemetryCount("datadog.dogstatsd.client.packets_dropped_queue", int64(senderMetrics.TotalDroppedPayloadsQueueFull), t.tags)
telemetryCount("datadog.dogstatsd.client.bytes_dropped_queue", int64(senderMetrics.TotalDroppedBytesQueueFull), t.tags)
telemetryCount("datadog.dogstatsd.client.packets_dropped_writer", int64(senderMetrics.TotalDroppedPayloadsWriter), t.tags)
telemetryCount("datadog.dogstatsd.client.bytes_dropped_writer", int64(senderMetrics.TotalDroppedBytesWriter), t.tags)
if aggMetrics := t.c.agg.flushTelemetryMetrics(); aggMetrics != nil {
telemetryCount("datadog.dogstatsd.client.aggregated_context", int64(aggMetrics.nbContext), t.tags)
if t.devMode {
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextGauge), t.tagsByType[gauge])
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextSet), t.tagsByType[set])
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextCount), t.tagsByType[count])
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextHistogram), t.tagsByType[histogram])
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextDistribution), t.tagsByType[distribution])
telemetryCount("datadog.dogstatsd.client.aggregated_context_by_type", int64(aggMetrics.nbContextTiming), t.tagsByType[timing])
}
}
return m
}

40
vendor/github.com/DataDog/datadog-go/statsd/udp.go generated vendored Normal file
View File

@@ -0,0 +1,40 @@
package statsd
import (
"errors"
"net"
"time"
)
// udpWriter is an internal class wrapping around management of UDP connection
type udpWriter struct {
conn net.Conn
}
// New returns a pointer to a new udpWriter given an addr in the format "hostname:port".
func newUDPWriter(addr string) (*udpWriter, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
writer := &udpWriter{conn: conn}
return writer, nil
}
// SetWriteTimeout is not needed for UDP, returns error
func (w *udpWriter) SetWriteTimeout(d time.Duration) error {
return errors.New("SetWriteTimeout: not supported for UDP connections")
}
// Write data to the UDP connection with no error handling
func (w *udpWriter) Write(data []byte) (int, error) {
return w.conn.Write(data)
}
func (w *udpWriter) Close() error {
return w.conn.Close()
}

100
vendor/github.com/DataDog/datadog-go/statsd/uds.go generated vendored Normal file
View File

@@ -0,0 +1,100 @@
// +build !windows
package statsd
import (
"net"
"sync"
"time"
)
/*
UDSTimeout holds the default timeout for UDS socket writes, as they can get
blocking when the receiving buffer is full.
*/
const defaultUDSTimeout = 100 * time.Millisecond
// udsWriter is an internal class wrapping around management of UDS connection
type udsWriter struct {
// Address to send metrics to, needed to allow reconnection on error
addr net.Addr
// Established connection object, or nil if not connected yet
conn net.Conn
// write timeout
writeTimeout time.Duration
sync.RWMutex // used to lock conn / writer can replace it
}
// newUDSWriter returns a pointer to a new udsWriter given a socket file path as addr.
func newUDSWriter(addr string) (*udsWriter, error) {
udsAddr, err := net.ResolveUnixAddr("unixgram", addr)
if err != nil {
return nil, err
}
// Defer connection to first Write
writer := &udsWriter{addr: udsAddr, conn: nil, writeTimeout: defaultUDSTimeout}
return writer, nil
}
// SetWriteTimeout allows the user to set a custom write timeout
func (w *udsWriter) SetWriteTimeout(d time.Duration) error {
w.writeTimeout = d
return nil
}
// Write data to the UDS connection with write timeout and minimal error handling:
// create the connection if nil, and destroy it if the statsd server has disconnected
func (w *udsWriter) Write(data []byte) (int, error) {
conn, err := w.ensureConnection()
if err != nil {
return 0, err
}
conn.SetWriteDeadline(time.Now().Add(w.writeTimeout))
n, e := conn.Write(data)
if err, isNetworkErr := e.(net.Error); err != nil && (!isNetworkErr || !err.Temporary()) {
// Statsd server disconnected, retry connecting at next packet
w.unsetConnection()
return 0, e
}
return n, e
}
func (w *udsWriter) Close() error {
if w.conn != nil {
return w.conn.Close()
}
return nil
}
func (w *udsWriter) ensureConnection() (net.Conn, error) {
// Check if we've already got a socket we can use
w.RLock()
currentConn := w.conn
w.RUnlock()
if currentConn != nil {
return currentConn, nil
}
// Looks like we might need to connect - try again with write locking.
w.Lock()
defer w.Unlock()
if w.conn != nil {
return w.conn, nil
}
newConn, err := net.Dial(w.addr.Network(), w.addr.String())
if err != nil {
return nil, err
}
w.conn = newConn
return newConn, nil
}
func (w *udsWriter) unsetConnection() {
w.Lock()
defer w.Unlock()
w.conn = nil
}

View File

@@ -0,0 +1,10 @@
// +build windows
package statsd
import "fmt"
// newUDSWriter is disable on windows as unix sockets are not available
func newUDSWriter(addr string) (statsdWriter, error) {
return nil, fmt.Errorf("unix socket is not available on windows")
}

23
vendor/github.com/DataDog/datadog-go/statsd/utils.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
package statsd
import (
"math/rand"
"sync"
)
func shouldSample(rate float64, r *rand.Rand, lock *sync.Mutex) bool {
if rate >= 1 {
return true
}
// sources created by rand.NewSource() (ie. w.random) are not thread safe.
// TODO: use defer once the lowest Go version we support is 1.14 (defer
// has an overhead before that).
lock.Lock()
if r.Float64() > rate {
lock.Unlock()
return false
}
lock.Unlock()
return true
}

150
vendor/github.com/DataDog/datadog-go/statsd/worker.go generated vendored Normal file
View File

@@ -0,0 +1,150 @@
package statsd
import (
"math/rand"
"sync"
"time"
)
type worker struct {
pool *bufferPool
buffer *statsdBuffer
sender *sender
random *rand.Rand
randomLock sync.Mutex
sync.Mutex
inputMetrics chan metric
stop chan struct{}
}
func newWorker(pool *bufferPool, sender *sender) *worker {
// Each worker uses its own random source and random lock to prevent
// workers in separate goroutines from contending for the lock on the
// "math/rand" package-global random source (e.g. calls like
// "rand.Float64()" must acquire a shared lock to get the next
// pseudorandom number).
// Note that calling "time.Now().UnixNano()" repeatedly quickly may return
// very similar values. That's fine for seeding the worker-specific random
// source because we just need an evenly distributed stream of float values.
// Do not use this random source for cryptographic randomness.
random := rand.New(rand.NewSource(time.Now().UnixNano()))
return &worker{
pool: pool,
sender: sender,
buffer: pool.borrowBuffer(),
random: random,
stop: make(chan struct{}),
}
}
func (w *worker) startReceivingMetric(bufferSize int) {
w.inputMetrics = make(chan metric, bufferSize)
go w.pullMetric()
}
func (w *worker) stopReceivingMetric() {
w.stop <- struct{}{}
}
func (w *worker) pullMetric() {
for {
select {
case m := <-w.inputMetrics:
w.processMetric(m)
case <-w.stop:
return
}
}
}
func (w *worker) processMetric(m metric) error {
if !shouldSample(m.rate, w.random, &w.randomLock) {
return nil
}
w.Lock()
var err error
if err = w.writeMetricUnsafe(m); err == errBufferFull {
w.flushUnsafe()
err = w.writeMetricUnsafe(m)
}
w.Unlock()
return err
}
func (w *worker) writeAggregatedMetricUnsafe(m metric, metricSymbol []byte, precision int) error {
globalPos := 0
// first check how much data we can write to the buffer:
// +3 + len(metricSymbol) because the message will include '|<metricSymbol>|#' before the tags
// +1 for the potential line break at the start of the metric
tagsSize := len(m.stags) + 4 + len(metricSymbol)
for _, t := range m.globalTags {
tagsSize += len(t) + 1
}
for {
pos, err := w.buffer.writeAggregated(metricSymbol, m.namespace, m.globalTags, m.name, m.fvalues[globalPos:], m.stags, tagsSize, precision)
if err == errPartialWrite {
// We successfully wrote part of the histogram metrics.
// We flush the current buffer and finish the histogram
// in a new one.
w.flushUnsafe()
globalPos += pos
} else {
return err
}
}
}
func (w *worker) writeMetricUnsafe(m metric) error {
switch m.metricType {
case gauge:
return w.buffer.writeGauge(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
case count:
return w.buffer.writeCount(m.namespace, m.globalTags, m.name, m.ivalue, m.tags, m.rate)
case histogram:
return w.buffer.writeHistogram(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
case distribution:
return w.buffer.writeDistribution(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
case set:
return w.buffer.writeSet(m.namespace, m.globalTags, m.name, m.svalue, m.tags, m.rate)
case timing:
return w.buffer.writeTiming(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
case event:
return w.buffer.writeEvent(*m.evalue, m.globalTags)
case serviceCheck:
return w.buffer.writeServiceCheck(*m.scvalue, m.globalTags)
case histogramAggregated:
return w.writeAggregatedMetricUnsafe(m, histogramSymbol, -1)
case distributionAggregated:
return w.writeAggregatedMetricUnsafe(m, distributionSymbol, -1)
case timingAggregated:
return w.writeAggregatedMetricUnsafe(m, timingSymbol, 6)
default:
return nil
}
}
func (w *worker) flush() {
w.Lock()
w.flushUnsafe()
w.Unlock()
}
func (w *worker) pause() {
w.Lock()
}
func (w *worker) unpause() {
w.Unlock()
}
// flush the current buffer. Lock must be held by caller.
// flushed buffer written to the network asynchronously.
func (w *worker) flushUnsafe() {
if len(w.buffer.bytes()) > 0 {
w.sender.send(w.buffer)
w.buffer = w.pool.borrowBuffer()
}
}

View File

@@ -1,6 +1,4 @@
The MIT License (MIT)
Copyright (c) 2014 Evan Huus
Copyright (c) 2015 Datadog, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,4 @@
## Overview
Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags
and histograms.

View File

@@ -0,0 +1,269 @@
package statsd
import (
"strings"
"sync"
"sync/atomic"
"time"
)
type (
countsMap map[string]*countMetric
gaugesMap map[string]*gaugeMetric
setsMap map[string]*setMetric
bufferedMetricMap map[string]*bufferedMetric
)
type aggregator struct {
nbContextGauge uint64
nbContextCount uint64
nbContextSet uint64
countsM sync.RWMutex
gaugesM sync.RWMutex
setsM sync.RWMutex
gauges gaugesMap
counts countsMap
sets setsMap
histograms bufferedMetricContexts
distributions bufferedMetricContexts
timings bufferedMetricContexts
closed chan struct{}
client *Client
// aggregator implements channelMode mechanism to receive histograms,
// distributions and timings. Since they need sampling they need to
// lock for random. When using both channelMode and ExtendedAggregation
// we don't want goroutine to fight over the lock.
inputMetrics chan metric
stopChannelMode chan struct{}
wg sync.WaitGroup
}
func newAggregator(c *Client) *aggregator {
return &aggregator{
client: c,
counts: countsMap{},
gauges: gaugesMap{},
sets: setsMap{},
histograms: newBufferedContexts(newHistogramMetric),
distributions: newBufferedContexts(newDistributionMetric),
timings: newBufferedContexts(newTimingMetric),
closed: make(chan struct{}),
stopChannelMode: make(chan struct{}),
}
}
func (a *aggregator) start(flushInterval time.Duration) {
ticker := time.NewTicker(flushInterval)
go func() {
for {
select {
case <-ticker.C:
a.flush()
case <-a.closed:
return
}
}
}()
}
func (a *aggregator) startReceivingMetric(bufferSize int, nbWorkers int) {
a.inputMetrics = make(chan metric, bufferSize)
for i := 0; i < nbWorkers; i++ {
a.wg.Add(1)
go a.pullMetric()
}
}
func (a *aggregator) stopReceivingMetric() {
close(a.stopChannelMode)
a.wg.Wait()
}
func (a *aggregator) stop() {
a.closed <- struct{}{}
}
func (a *aggregator) pullMetric() {
for {
select {
case m := <-a.inputMetrics:
switch m.metricType {
case histogram:
a.histogram(m.name, m.fvalue, m.tags, m.rate)
case distribution:
a.distribution(m.name, m.fvalue, m.tags, m.rate)
case timing:
a.timing(m.name, m.fvalue, m.tags, m.rate)
}
case <-a.stopChannelMode:
a.wg.Done()
return
}
}
}
func (a *aggregator) flush() {
for _, m := range a.flushMetrics() {
a.client.sendBlocking(m)
}
}
func (a *aggregator) flushTelemetryMetrics(t *Telemetry) {
if a == nil {
// aggregation is disabled
return
}
t.AggregationNbContextGauge = atomic.LoadUint64(&a.nbContextGauge)
t.AggregationNbContextCount = atomic.LoadUint64(&a.nbContextCount)
t.AggregationNbContextSet = atomic.LoadUint64(&a.nbContextSet)
t.AggregationNbContextHistogram = a.histograms.getNbContext()
t.AggregationNbContextDistribution = a.distributions.getNbContext()
t.AggregationNbContextTiming = a.timings.getNbContext()
}
func (a *aggregator) flushMetrics() []metric {
metrics := []metric{}
// We reset the values to avoid sending 'zero' values for metrics not
// sampled during this flush interval
a.setsM.Lock()
sets := a.sets
a.sets = setsMap{}
a.setsM.Unlock()
for _, s := range sets {
metrics = append(metrics, s.flushUnsafe()...)
}
a.gaugesM.Lock()
gauges := a.gauges
a.gauges = gaugesMap{}
a.gaugesM.Unlock()
for _, g := range gauges {
metrics = append(metrics, g.flushUnsafe())
}
a.countsM.Lock()
counts := a.counts
a.counts = countsMap{}
a.countsM.Unlock()
for _, c := range counts {
metrics = append(metrics, c.flushUnsafe())
}
metrics = a.histograms.flush(metrics)
metrics = a.distributions.flush(metrics)
metrics = a.timings.flush(metrics)
atomic.AddUint64(&a.nbContextCount, uint64(len(counts)))
atomic.AddUint64(&a.nbContextGauge, uint64(len(gauges)))
atomic.AddUint64(&a.nbContextSet, uint64(len(sets)))
return metrics
}
func getContext(name string, tags []string) string {
return name + ":" + strings.Join(tags, tagSeparatorSymbol)
}
func getContextAndTags(name string, tags []string) (string, string) {
stringTags := strings.Join(tags, tagSeparatorSymbol)
return name + ":" + stringTags, stringTags
}
func (a *aggregator) count(name string, value int64, tags []string) error {
context := getContext(name, tags)
a.countsM.RLock()
if count, found := a.counts[context]; found {
count.sample(value)
a.countsM.RUnlock()
return nil
}
a.countsM.RUnlock()
a.countsM.Lock()
// Check if another goroutines hasn't created the value betwen the RUnlock and 'Lock'
if count, found := a.counts[context]; found {
count.sample(value)
a.countsM.Unlock()
return nil
}
a.counts[context] = newCountMetric(name, value, tags)
a.countsM.Unlock()
return nil
}
func (a *aggregator) gauge(name string, value float64, tags []string) error {
context := getContext(name, tags)
a.gaugesM.RLock()
if gauge, found := a.gauges[context]; found {
gauge.sample(value)
a.gaugesM.RUnlock()
return nil
}
a.gaugesM.RUnlock()
gauge := newGaugeMetric(name, value, tags)
a.gaugesM.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if gauge, found := a.gauges[context]; found {
gauge.sample(value)
a.gaugesM.Unlock()
return nil
}
a.gauges[context] = gauge
a.gaugesM.Unlock()
return nil
}
func (a *aggregator) set(name string, value string, tags []string) error {
context := getContext(name, tags)
a.setsM.RLock()
if set, found := a.sets[context]; found {
set.sample(value)
a.setsM.RUnlock()
return nil
}
a.setsM.RUnlock()
a.setsM.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if set, found := a.sets[context]; found {
set.sample(value)
a.setsM.Unlock()
return nil
}
a.sets[context] = newSetMetric(name, value, tags)
a.setsM.Unlock()
return nil
}
// Only histograms, distributions and timings are sampled with a rate since we
// only pack them in on message instead of aggregating them. Discarding the
// sample rate will have impacts on the CPU and memory usage of the Agent.
// type alias for Client.sendToAggregator
type bufferedMetricSampleFunc func(name string, value float64, tags []string, rate float64) error
func (a *aggregator) histogram(name string, value float64, tags []string, rate float64) error {
return a.histograms.sample(name, value, tags, rate)
}
func (a *aggregator) distribution(name string, value float64, tags []string, rate float64) error {
return a.distributions.sample(name, value, tags, rate)
}
func (a *aggregator) timing(name string, value float64, tags []string, rate float64) error {
return a.timings.sample(name, value, tags, rate)
}

View File

@@ -0,0 +1,190 @@
package statsd
import (
"strconv"
)
type bufferFullError string
func (e bufferFullError) Error() string { return string(e) }
const errBufferFull = bufferFullError("statsd buffer is full")
type partialWriteError string
func (e partialWriteError) Error() string { return string(e) }
const errPartialWrite = partialWriteError("value partially written")
const metricOverhead = 512
// statsdBuffer is a buffer containing statsd messages
// this struct methods are NOT safe for concurent use
type statsdBuffer struct {
buffer []byte
maxSize int
maxElements int
elementCount int
}
func newStatsdBuffer(maxSize, maxElements int) *statsdBuffer {
return &statsdBuffer{
buffer: make([]byte, 0, maxSize+metricOverhead), // pre-allocate the needed size + metricOverhead to avoid having Go re-allocate on it's own if an element does not fit
maxSize: maxSize,
maxElements: maxElements,
}
}
func (b *statsdBuffer) writeGauge(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendGauge(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeCount(namespace string, globalTags []string, name string, value int64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendCount(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeHistogram(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendHistogram(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
// writeAggregated serialized as many values as possible in the current buffer and return the position in values where it stopped.
func (b *statsdBuffer) writeAggregated(metricSymbol []byte, namespace string, globalTags []string, name string, values []float64, tags string, tagSize int, precision int) (int, error) {
if b.elementCount >= b.maxElements {
return 0, errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendHeader(b.buffer, namespace, name)
// buffer already full
if len(b.buffer)+tagSize > b.maxSize {
b.buffer = originalBuffer
return 0, errBufferFull
}
// We add as many value as possible
var position int
for idx, v := range values {
previousBuffer := b.buffer
if idx != 0 {
b.buffer = append(b.buffer, ':')
}
b.buffer = strconv.AppendFloat(b.buffer, v, 'f', precision, 64)
// Should we stop serializing and switch to another buffer
if len(b.buffer)+tagSize > b.maxSize {
b.buffer = previousBuffer
break
}
position = idx + 1
}
// we could not add a single value
if position == 0 {
b.buffer = originalBuffer
return 0, errBufferFull
}
b.buffer = append(b.buffer, '|')
b.buffer = append(b.buffer, metricSymbol...)
b.buffer = appendTagsAggregated(b.buffer, globalTags, tags)
b.writeSeparator()
b.elementCount++
if position != len(values) {
return position, errPartialWrite
}
return position, nil
}
func (b *statsdBuffer) writeDistribution(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendDistribution(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeSet(namespace string, globalTags []string, name string, value string, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendSet(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeTiming(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendTiming(b.buffer, namespace, globalTags, name, value, tags, rate)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeEvent(event *Event, globalTags []string) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendEvent(b.buffer, event, globalTags)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) writeServiceCheck(serviceCheck *ServiceCheck, globalTags []string) error {
if b.elementCount >= b.maxElements {
return errBufferFull
}
originalBuffer := b.buffer
b.buffer = appendServiceCheck(b.buffer, serviceCheck, globalTags)
b.writeSeparator()
return b.validateNewElement(originalBuffer)
}
func (b *statsdBuffer) validateNewElement(originalBuffer []byte) error {
if len(b.buffer) > b.maxSize {
b.buffer = originalBuffer
return errBufferFull
}
b.elementCount++
return nil
}
func (b *statsdBuffer) writeSeparator() {
b.buffer = append(b.buffer, '\n')
}
func (b *statsdBuffer) reset() {
b.buffer = b.buffer[:0]
b.elementCount = 0
}
func (b *statsdBuffer) bytes() []byte {
return b.buffer
}

View File

@@ -0,0 +1,40 @@
package statsd
type bufferPool struct {
pool chan *statsdBuffer
bufferMaxSize int
bufferMaxElements int
}
func newBufferPool(poolSize, bufferMaxSize, bufferMaxElements int) *bufferPool {
p := &bufferPool{
pool: make(chan *statsdBuffer, poolSize),
bufferMaxSize: bufferMaxSize,
bufferMaxElements: bufferMaxElements,
}
for i := 0; i < poolSize; i++ {
p.addNewBuffer()
}
return p
}
func (p *bufferPool) addNewBuffer() {
p.pool <- newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
}
func (p *bufferPool) borrowBuffer() *statsdBuffer {
select {
case b := <-p.pool:
return b
default:
return newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
}
}
func (p *bufferPool) returnBuffer(buffer *statsdBuffer) {
buffer.reset()
select {
case p.pool <- buffer:
default:
}
}

View File

@@ -0,0 +1,82 @@
package statsd
import (
"math/rand"
"sync"
"sync/atomic"
"time"
)
// bufferedMetricContexts represent the contexts for Histograms, Distributions
// and Timing. Since those 3 metric types behave the same way and are sampled
// with the same type they're represented by the same class.
type bufferedMetricContexts struct {
nbContext uint64
mutex sync.RWMutex
values bufferedMetricMap
newMetric func(string, float64, string) *bufferedMetric
// Each bufferedMetricContexts uses its own random source and random
// lock to prevent goroutines from contending for the lock on the
// "math/rand" package-global random source (e.g. calls like
// "rand.Float64()" must acquire a shared lock to get the next
// pseudorandom number).
random *rand.Rand
randomLock sync.Mutex
}
func newBufferedContexts(newMetric func(string, float64, string) *bufferedMetric) bufferedMetricContexts {
return bufferedMetricContexts{
values: bufferedMetricMap{},
newMetric: newMetric,
// Note that calling "time.Now().UnixNano()" repeatedly quickly may return
// very similar values. That's fine for seeding the worker-specific random
// source because we just need an evenly distributed stream of float values.
// Do not use this random source for cryptographic randomness.
random: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
func (bc *bufferedMetricContexts) flush(metrics []metric) []metric {
bc.mutex.Lock()
values := bc.values
bc.values = bufferedMetricMap{}
bc.mutex.Unlock()
for _, d := range values {
metrics = append(metrics, d.flushUnsafe())
}
atomic.AddUint64(&bc.nbContext, uint64(len(values)))
return metrics
}
func (bc *bufferedMetricContexts) sample(name string, value float64, tags []string, rate float64) error {
if !shouldSample(rate, bc.random, &bc.randomLock) {
return nil
}
context, stringTags := getContextAndTags(name, tags)
bc.mutex.RLock()
if v, found := bc.values[context]; found {
v.sample(value)
bc.mutex.RUnlock()
return nil
}
bc.mutex.RUnlock()
bc.mutex.Lock()
// Check if another goroutines hasn't created the value betwen the 'RUnlock' and 'Lock'
if v, found := bc.values[context]; found {
v.sample(value)
bc.mutex.Unlock()
return nil
}
bc.values[context] = bc.newMetric(name, value, stringTags)
bc.mutex.Unlock()
return nil
}
func (bc *bufferedMetricContexts) getNbContext() uint64 {
return atomic.LoadUint64(&bc.nbContext)
}

View File

@@ -0,0 +1,75 @@
package statsd
import (
"fmt"
"time"
)
// Events support
// EventAlertType and EventAlertPriority became exported types after this issue was submitted: https://github.com/DataDog/datadog-go/issues/41
// The reason why they got exported is so that client code can directly use the types.
// EventAlertType is the alert type for events
type EventAlertType string
const (
// Info is the "info" AlertType for events
Info EventAlertType = "info"
// Error is the "error" AlertType for events
Error EventAlertType = "error"
// Warning is the "warning" AlertType for events
Warning EventAlertType = "warning"
// Success is the "success" AlertType for events
Success EventAlertType = "success"
)
// EventPriority is the event priority for events
type EventPriority string
const (
// Normal is the "normal" Priority for events
Normal EventPriority = "normal"
// Low is the "low" Priority for events
Low EventPriority = "low"
)
// An Event is an object that can be posted to your DataDog event stream.
type Event struct {
// Title of the event. Required.
Title string
// Text is the description of the event. Required.
Text string
// Timestamp is a timestamp for the event. If not provided, the dogstatsd
// server will set this to the current time.
Timestamp time.Time
// Hostname for the event.
Hostname string
// AggregationKey groups this event with others of the same key.
AggregationKey string
// Priority of the event. Can be statsd.Low or statsd.Normal.
Priority EventPriority
// SourceTypeName is a source type for the event.
SourceTypeName string
// AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success.
// If absent, the default value applied by the dogstatsd server is Info.
AlertType EventAlertType
// Tags for the event.
Tags []string
}
// NewEvent creates a new event with the given title and text. Error checking
// against these values is done at send-time, or upon running e.Check.
func NewEvent(title, text string) *Event {
return &Event{
Title: title,
Text: text,
}
}
// Check verifies that an event is valid.
func (e *Event) Check() error {
if len(e.Title) == 0 {
return fmt.Errorf("statsd.Event title is required")
}
return nil
}

View File

@@ -0,0 +1,39 @@
package statsd
const (
// FNV-1a
offset32 = uint32(2166136261)
prime32 = uint32(16777619)
// init32 is what 32 bits hash values should be initialized with.
init32 = offset32
)
// HashString32 returns the hash of s.
func hashString32(s string) uint32 {
return addString32(init32, s)
}
// AddString32 adds the hash of s to the precomputed hash value h.
func addString32(h uint32, s string) uint32 {
i := 0
n := (len(s) / 8) * 8
for i != n {
h = (h ^ uint32(s[i])) * prime32
h = (h ^ uint32(s[i+1])) * prime32
h = (h ^ uint32(s[i+2])) * prime32
h = (h ^ uint32(s[i+3])) * prime32
h = (h ^ uint32(s[i+4])) * prime32
h = (h ^ uint32(s[i+5])) * prime32
h = (h ^ uint32(s[i+6])) * prime32
h = (h ^ uint32(s[i+7])) * prime32
i += 8
}
for _, c := range s[i:] {
h = (h ^ uint32(c)) * prime32
}
return h
}

View File

@@ -0,0 +1,257 @@
package statsd
import (
"strconv"
"strings"
)
var (
gaugeSymbol = []byte("g")
countSymbol = []byte("c")
histogramSymbol = []byte("h")
distributionSymbol = []byte("d")
setSymbol = []byte("s")
timingSymbol = []byte("ms")
tagSeparatorSymbol = ","
)
func appendHeader(buffer []byte, namespace string, name string) []byte {
if namespace != "" {
buffer = append(buffer, namespace...)
}
buffer = append(buffer, name...)
buffer = append(buffer, ':')
return buffer
}
func appendRate(buffer []byte, rate float64) []byte {
if rate < 1 {
buffer = append(buffer, "|@"...)
buffer = strconv.AppendFloat(buffer, rate, 'f', -1, 64)
}
return buffer
}
func appendWithoutNewlines(buffer []byte, s string) []byte {
// fastpath for strings without newlines
if strings.IndexByte(s, '\n') == -1 {
return append(buffer, s...)
}
for _, b := range []byte(s) {
if b != '\n' {
buffer = append(buffer, b)
}
}
return buffer
}
func appendTags(buffer []byte, globalTags []string, tags []string) []byte {
if len(globalTags) == 0 && len(tags) == 0 {
return buffer
}
buffer = append(buffer, "|#"...)
firstTag := true
for _, tag := range globalTags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
for _, tag := range tags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
return buffer
}
func appendTagsAggregated(buffer []byte, globalTags []string, tags string) []byte {
if len(globalTags) == 0 && tags == "" {
return buffer
}
buffer = append(buffer, "|#"...)
firstTag := true
for _, tag := range globalTags {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tag)
firstTag = false
}
if tags != "" {
if !firstTag {
buffer = append(buffer, tagSeparatorSymbol...)
}
buffer = appendWithoutNewlines(buffer, tags)
}
return buffer
}
func appendFloatMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, precision int) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = strconv.AppendFloat(buffer, value, 'f', precision, 64)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendIntegerMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = strconv.AppendInt(buffer, value, 10)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendStringMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
buffer = appendHeader(buffer, namespace, name)
buffer = append(buffer, value...)
buffer = append(buffer, '|')
buffer = append(buffer, typeSymbol...)
buffer = appendRate(buffer, rate)
buffer = appendTags(buffer, globalTags, tags)
return buffer
}
func appendGauge(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, gaugeSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendCount(buffer []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
return appendIntegerMetric(buffer, countSymbol, namespace, globalTags, name, value, tags, rate)
}
func appendHistogram(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, histogramSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendDistribution(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, distributionSymbol, namespace, globalTags, name, value, tags, rate, -1)
}
func appendSet(buffer []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
return appendStringMetric(buffer, setSymbol, namespace, globalTags, name, value, tags, rate)
}
func appendTiming(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
return appendFloatMetric(buffer, timingSymbol, namespace, globalTags, name, value, tags, rate, 6)
}
func escapedEventTextLen(text string) int {
return len(text) + strings.Count(text, "\n")
}
func appendEscapedEventText(buffer []byte, text string) []byte {
for _, b := range []byte(text) {
if b != '\n' {
buffer = append(buffer, b)
} else {
buffer = append(buffer, "\\n"...)
}
}
return buffer
}
func appendEvent(buffer []byte, event *Event, globalTags []string) []byte {
escapedTextLen := escapedEventTextLen(event.Text)
buffer = append(buffer, "_e{"...)
buffer = strconv.AppendInt(buffer, int64(len(event.Title)), 10)
buffer = append(buffer, tagSeparatorSymbol...)
buffer = strconv.AppendInt(buffer, int64(escapedTextLen), 10)
buffer = append(buffer, "}:"...)
buffer = append(buffer, event.Title...)
buffer = append(buffer, '|')
if escapedTextLen != len(event.Text) {
buffer = appendEscapedEventText(buffer, event.Text)
} else {
buffer = append(buffer, event.Text...)
}
if !event.Timestamp.IsZero() {
buffer = append(buffer, "|d:"...)
buffer = strconv.AppendInt(buffer, int64(event.Timestamp.Unix()), 10)
}
if len(event.Hostname) != 0 {
buffer = append(buffer, "|h:"...)
buffer = append(buffer, event.Hostname...)
}
if len(event.AggregationKey) != 0 {
buffer = append(buffer, "|k:"...)
buffer = append(buffer, event.AggregationKey...)
}
if len(event.Priority) != 0 {
buffer = append(buffer, "|p:"...)
buffer = append(buffer, event.Priority...)
}
if len(event.SourceTypeName) != 0 {
buffer = append(buffer, "|s:"...)
buffer = append(buffer, event.SourceTypeName...)
}
if len(event.AlertType) != 0 {
buffer = append(buffer, "|t:"...)
buffer = append(buffer, string(event.AlertType)...)
}
buffer = appendTags(buffer, globalTags, event.Tags)
return buffer
}
func appendEscapedServiceCheckText(buffer []byte, text string) []byte {
for i := 0; i < len(text); i++ {
if text[i] == '\n' {
buffer = append(buffer, "\\n"...)
} else if text[i] == 'm' && i+1 < len(text) && text[i+1] == ':' {
buffer = append(buffer, "m\\:"...)
i++
} else {
buffer = append(buffer, text[i])
}
}
return buffer
}
func appendServiceCheck(buffer []byte, serviceCheck *ServiceCheck, globalTags []string) []byte {
buffer = append(buffer, "_sc|"...)
buffer = append(buffer, serviceCheck.Name...)
buffer = append(buffer, '|')
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Status), 10)
if !serviceCheck.Timestamp.IsZero() {
buffer = append(buffer, "|d:"...)
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Timestamp.Unix()), 10)
}
if len(serviceCheck.Hostname) != 0 {
buffer = append(buffer, "|h:"...)
buffer = append(buffer, serviceCheck.Hostname...)
}
buffer = appendTags(buffer, globalTags, serviceCheck.Tags)
if len(serviceCheck.Message) != 0 {
buffer = append(buffer, "|m:"...)
buffer = appendEscapedServiceCheckText(buffer, serviceCheck.Message)
}
return buffer
}
func appendSeparator(buffer []byte) []byte {
return append(buffer, '\n')
}

View File

@@ -0,0 +1,181 @@
package statsd
import (
"math"
"sync"
"sync/atomic"
)
/*
Those are metrics type that can be aggregated on the client side:
- Gauge
- Count
- Set
*/
type countMetric struct {
value int64
name string
tags []string
}
func newCountMetric(name string, value int64, tags []string) *countMetric {
return &countMetric{
value: value,
name: name,
tags: tags,
}
}
func (c *countMetric) sample(v int64) {
atomic.AddInt64(&c.value, v)
}
func (c *countMetric) flushUnsafe() metric {
return metric{
metricType: count,
name: c.name,
tags: c.tags,
rate: 1,
ivalue: c.value,
}
}
// Gauge
type gaugeMetric struct {
value uint64
name string
tags []string
}
func newGaugeMetric(name string, value float64, tags []string) *gaugeMetric {
return &gaugeMetric{
value: math.Float64bits(value),
name: name,
tags: tags,
}
}
func (g *gaugeMetric) sample(v float64) {
atomic.StoreUint64(&g.value, math.Float64bits(v))
}
func (g *gaugeMetric) flushUnsafe() metric {
return metric{
metricType: gauge,
name: g.name,
tags: g.tags,
rate: 1,
fvalue: math.Float64frombits(g.value),
}
}
// Set
type setMetric struct {
data map[string]struct{}
name string
tags []string
sync.Mutex
}
func newSetMetric(name string, value string, tags []string) *setMetric {
set := &setMetric{
data: map[string]struct{}{},
name: name,
tags: tags,
}
set.data[value] = struct{}{}
return set
}
func (s *setMetric) sample(v string) {
s.Lock()
defer s.Unlock()
s.data[v] = struct{}{}
}
// Sets are aggregated on the agent side too. We flush the keys so a set from
// multiple application can be correctly aggregated on the agent side.
func (s *setMetric) flushUnsafe() []metric {
if len(s.data) == 0 {
return nil
}
metrics := make([]metric, len(s.data))
i := 0
for value := range s.data {
metrics[i] = metric{
metricType: set,
name: s.name,
tags: s.tags,
rate: 1,
svalue: value,
}
i++
}
return metrics
}
// Histograms, Distributions and Timings
type bufferedMetric struct {
sync.Mutex
data []float64
name string
// Histograms and Distributions store tags as one string since we need
// to compute its size multiple time when serializing.
tags string
mtype metricType
}
func (s *bufferedMetric) sample(v float64) {
s.Lock()
defer s.Unlock()
s.data = append(s.data, v)
}
func (s *bufferedMetric) flushUnsafe() metric {
return metric{
metricType: s.mtype,
name: s.name,
stags: s.tags,
rate: 1,
fvalues: s.data,
}
}
type histogramMetric = bufferedMetric
func newHistogramMetric(name string, value float64, stringTags string) *histogramMetric {
return &histogramMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: histogramAggregated,
}
}
type distributionMetric = bufferedMetric
func newDistributionMetric(name string, value float64, stringTags string) *distributionMetric {
return &distributionMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: distributionAggregated,
}
}
type timingMetric = bufferedMetric
func newTimingMetric(name string, value float64, stringTags string) *timingMetric {
return &timingMetric{
data: []float64{value},
name: name,
tags: stringTags,
mtype: timingAggregated,
}
}

Some files were not shown because too many files have changed in this diff Show More