vendor: Update vault api package
This commit is contained in:
parent
63d3a5fc1f
commit
c3bd917650
15
go.mod
15
go.mod
|
@ -22,6 +22,7 @@ require (
|
|||
github.com/docker/go-connections v0.3.0
|
||||
github.com/elazarl/go-bindata-assetfs v0.0.0-20160803192304-e1a2a7ec64b0
|
||||
github.com/envoyproxy/go-control-plane v0.9.5
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible // indirect
|
||||
github.com/go-ole/go-ole v1.2.1 // indirect
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d
|
||||
github.com/golang/protobuf v1.3.5
|
||||
|
@ -42,6 +43,7 @@ require (
|
|||
github.com/hashicorp/go-msgpack v0.5.5
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-raftchunking v0.6.1
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.2
|
||||
github.com/hashicorp/go-syslog v1.0.0
|
||||
github.com/hashicorp/go-uuid v1.0.2
|
||||
|
@ -54,7 +56,7 @@ require (
|
|||
github.com/hashicorp/raft v1.1.2
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea
|
||||
github.com/hashicorp/serf v0.9.4
|
||||
github.com/hashicorp/vault/api v1.0.4
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d
|
||||
github.com/imdario/mergo v0.3.6
|
||||
github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect
|
||||
|
@ -69,6 +71,7 @@ require (
|
|||
github.com/mitchellh/pointerstructure v1.0.0
|
||||
github.com/mitchellh/reflectwalk v1.0.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.4.0
|
||||
|
@ -78,18 +81,20 @@ require (
|
|||
github.com/stretchr/testify v1.5.1
|
||||
go.opencensus.io v0.22.0 // indirect
|
||||
go.uber.org/goleak v1.0.0
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
||||
golang.org/x/tools v0.0.0-20200513154647-78b527d18275 // indirect
|
||||
google.golang.org/api v0.9.0 // indirect
|
||||
google.golang.org/appengine v1.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
|
||||
google.golang.org/grpc v1.25.1
|
||||
gopkg.in/square/go-jose.v2 v2.4.1
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
k8s.io/api v0.16.9
|
||||
k8s.io/apimachinery v0.16.9
|
||||
k8s.io/client-go v0.16.9
|
||||
|
|
32
go.sum
32
go.sum
|
@ -60,11 +60,13 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
|
|||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
|
||||
github.com/armon/go-metrics v0.3.4 h1:Xqf+7f2Vhl9tsqDYmXhnXInUdcrtgpRNpIA15/uldSc=
|
||||
github.com/armon/go-metrics v0.3.4/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.25.41 h1:/hj7nZ0586wFqpwjNpzWiUTwtaMgxAZNZKHay80MdXw=
|
||||
github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
|
@ -139,10 +141,12 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
|||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-check/check v0.0.0-20140225173054-eb6ee6f84d0a/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
||||
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-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
|
@ -227,6 +231,7 @@ github.com/hashicorp/go-discover v0.0.0-20200501174627-ad1e96bde088/go.mod h1:vZ
|
|||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
|
@ -234,6 +239,7 @@ github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTME
|
|||
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.2.0 h1:l6UW37iCXwZkZoAbEYnptSHVE/cQ5bOTPYG5W3vf9+8=
|
||||
github.com/hashicorp/go-immutable-radix v1.2.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
|
||||
github.com/hashicorp/go-memdb v1.1.0 h1:ClvpUXpBA6UDs5+vc1h3wqe4UJU+rwum7CU219SeCbk=
|
||||
github.com/hashicorp/go-memdb v1.1.0/go.mod h1:LWQ8R70vPrS4OEY9k28D2z8/Zzyu34NVzeRibGAzHO0=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
|
@ -249,6 +255,10 @@ github.com/hashicorp/go-raftchunking v0.6.1/go.mod h1:cGlg3JtDy7qy6c/3Bu660Mic1J
|
|||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
|
@ -292,8 +302,12 @@ github.com/hashicorp/serf v0.9.4 h1:xrZ4ZR0wT5Dz8oQHHdfOzr0ei1jMToWlFFz3hh/DI7I=
|
|||
github.com/hashicorp/serf v0.9.4/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
|
||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 h1:OKsyxKi2sNmqm1Gv93adf2AID2FOBFdCbbZn9fGtIdg=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk=
|
||||
github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM=
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
|
||||
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw=
|
||||
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
|
@ -374,6 +388,7 @@ github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:
|
|||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.0.0 h1:ATSdz4NWrmWPOF1CeCBU4sMCno2hgqdbSrRPFWQSVZI=
|
||||
|
@ -413,6 +428,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
|
|||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
@ -531,6 +548,7 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/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=
|
||||
|
@ -539,6 +557,9 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947
|
|||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
@ -571,6 +592,10 @@ golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ym
|
|||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
|
@ -615,6 +640,7 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSf
|
|||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -623,11 +649,15 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
|||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -690,6 +720,8 @@ gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4
|
|||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
|
||||
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
Consul API client
|
||||
=================
|
||||
|
||||
This package provides the `api` package which attempts to
|
||||
provide programmatic access to the full Consul API.
|
||||
|
||||
Currently, all of the Consul APIs included in version 0.6.0 are supported.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
The full documentation is available on [Godoc](https://godoc.org/github.com/hashicorp/consul/api)
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Below is an example of using the Consul client:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/hashicorp/consul/api"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
// Get a new client
|
||||
client, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get a handle to the KV API
|
||||
kv := client.KV()
|
||||
|
||||
// PUT a new KV pair
|
||||
p := &api.KVPair{Key: "REDIS_MAXCLIENTS", Value: []byte("1000")}
|
||||
_, err = kv.Put(p, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Lookup the pair
|
||||
pair, _, err := kv.Get("REDIS_MAXCLIENTS", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("KV: %v %s\n", pair.Key, pair.Value)
|
||||
}
|
||||
```
|
||||
|
||||
To run this example, start a Consul server:
|
||||
|
||||
```bash
|
||||
consul agent -dev
|
||||
```
|
||||
|
||||
Copy the code above into a file such as `main.go`.
|
||||
|
||||
Install and run. You'll see a key (`REDIS_MAXCLIENTS`) and value (`1000`) printed.
|
||||
|
||||
```bash
|
||||
$ go get
|
||||
$ go run main.go
|
||||
KV: REDIS_MAXCLIENTS 1000
|
||||
```
|
||||
|
||||
After running the code, you can also view the values in the Consul UI on your local machine at http://localhost:8500/ui/dc1/kv
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,337 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Weights struct {
|
||||
Passing int
|
||||
Warning int
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string
|
||||
Node string
|
||||
Address string
|
||||
Datacenter string
|
||||
TaggedAddresses map[string]string
|
||||
Meta map[string]string
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type ServiceAddress struct {
|
||||
Address string
|
||||
Port int
|
||||
}
|
||||
|
||||
type CatalogService struct {
|
||||
ID string
|
||||
Node string
|
||||
Address string
|
||||
Datacenter string
|
||||
TaggedAddresses map[string]string
|
||||
NodeMeta map[string]string
|
||||
ServiceID string
|
||||
ServiceName string
|
||||
ServiceAddress string
|
||||
ServiceTaggedAddresses map[string]ServiceAddress
|
||||
ServiceTags []string
|
||||
ServiceMeta map[string]string
|
||||
ServicePort int
|
||||
ServiceWeights Weights
|
||||
ServiceEnableTagOverride bool
|
||||
ServiceProxy *AgentServiceConnectProxyConfig
|
||||
CreateIndex uint64
|
||||
Checks HealthChecks
|
||||
ModifyIndex uint64
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type CatalogNode struct {
|
||||
Node *Node
|
||||
Services map[string]*AgentService
|
||||
}
|
||||
|
||||
type CatalogNodeServiceList struct {
|
||||
Node *Node
|
||||
Services []*AgentService
|
||||
}
|
||||
|
||||
type CatalogRegistration struct {
|
||||
ID string
|
||||
Node string
|
||||
Address string
|
||||
TaggedAddresses map[string]string
|
||||
NodeMeta map[string]string
|
||||
Datacenter string
|
||||
Service *AgentService
|
||||
Check *AgentCheck
|
||||
Checks HealthChecks
|
||||
SkipNodeUpdate bool
|
||||
}
|
||||
|
||||
type CatalogDeregistration struct {
|
||||
Node string
|
||||
Address string `json:",omitempty"` // Obsolete.
|
||||
Datacenter string
|
||||
ServiceID string
|
||||
CheckID string
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type CompoundServiceName struct {
|
||||
Name string
|
||||
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// GatewayService associates a gateway with a linked service.
|
||||
// It also contains service-specific gateway configuration like ingress listener port and protocol.
|
||||
type GatewayService struct {
|
||||
Gateway CompoundServiceName
|
||||
Service CompoundServiceName
|
||||
GatewayKind ServiceKind
|
||||
Port int `json:",omitempty"`
|
||||
Protocol string `json:",omitempty"`
|
||||
Hosts []string `json:",omitempty"`
|
||||
CAFile string `json:",omitempty"`
|
||||
CertFile string `json:",omitempty"`
|
||||
KeyFile string `json:",omitempty"`
|
||||
SNI string `json:",omitempty"`
|
||||
FromWildcard bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Catalog can be used to query the Catalog endpoints
|
||||
type Catalog struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Catalog returns a handle to the catalog endpoints
|
||||
func (c *Client) Catalog() *Catalog {
|
||||
return &Catalog{c}
|
||||
}
|
||||
|
||||
func (c *Catalog) Register(reg *CatalogRegistration, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := c.c.newRequest("PUT", "/v1/catalog/register")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = reg
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) Deregister(dereg *CatalogDeregistration, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := c.c.newRequest("PUT", "/v1/catalog/deregister")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = dereg
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Datacenters is used to query for all the known datacenters
|
||||
func (c *Catalog) Datacenters() ([]string, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/datacenters")
|
||||
_, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out []string
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Nodes is used to query all the known nodes
|
||||
func (c *Catalog) Nodes(q *QueryOptions) ([]*Node, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/nodes")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*Node
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Services is used to query for all known services
|
||||
func (c *Catalog) Services(q *QueryOptions) (map[string][]string, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/services")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out map[string][]string
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Service is used to query catalog entries for a given service
|
||||
func (c *Catalog) Service(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return c.service(service, tags, q, false)
|
||||
}
|
||||
|
||||
// Supports multiple tags for filtering
|
||||
func (c *Catalog) ServiceMultipleTags(service string, tags []string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tags, q, false)
|
||||
}
|
||||
|
||||
// Connect is used to query catalog entries for a given Connect-enabled service
|
||||
func (c *Catalog) Connect(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return c.service(service, tags, q, true)
|
||||
}
|
||||
|
||||
// Supports multiple tags for filtering
|
||||
func (c *Catalog) ConnectMultipleTags(service string, tags []string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tags, q, true)
|
||||
}
|
||||
|
||||
func (c *Catalog) service(service string, tags []string, q *QueryOptions, connect bool) ([]*CatalogService, *QueryMeta, error) {
|
||||
path := "/v1/catalog/service/" + service
|
||||
if connect {
|
||||
path = "/v1/catalog/connect/" + service
|
||||
}
|
||||
r := c.c.newRequest("GET", path)
|
||||
r.setQueryOptions(q)
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
r.params.Add("tag", tag)
|
||||
}
|
||||
}
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*CatalogService
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Node is used to query for service information about a single node
|
||||
func (c *Catalog) Node(node string, q *QueryOptions) (*CatalogNode, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/node/"+node)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out *CatalogNode
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// NodeServiceList is used to query for service information about a single node. It differs from
|
||||
// the Node function only in its return type which will contain a list of services as opposed to
|
||||
// a map of service ids to services. This different structure allows for using the wildcard specifier
|
||||
// '*' for the Namespace in the QueryOptions.
|
||||
func (c *Catalog) NodeServiceList(node string, q *QueryOptions) (*CatalogNodeServiceList, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/node-services/"+node)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out *CatalogNodeServiceList
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// GatewayServices is used to query the services associated with an ingress gateway or terminating gateway.
|
||||
func (c *Catalog) GatewayServices(gateway string, q *QueryOptions) ([]*GatewayService, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/catalog/gateway-services/"+gateway)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*GatewayService
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
func ParseServiceAddr(addrPort string) (ServiceAddress, error) {
|
||||
port := 0
|
||||
host, portStr, err := net.SplitHostPort(addrPort)
|
||||
if err == nil {
|
||||
port, err = strconv.Atoi(portStr)
|
||||
}
|
||||
return ServiceAddress{Address: host, Port: port}, err
|
||||
}
|
|
@ -0,0 +1,353 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceDefaults string = "service-defaults"
|
||||
ProxyDefaults string = "proxy-defaults"
|
||||
ServiceRouter string = "service-router"
|
||||
ServiceSplitter string = "service-splitter"
|
||||
ServiceResolver string = "service-resolver"
|
||||
IngressGateway string = "ingress-gateway"
|
||||
TerminatingGateway string = "terminating-gateway"
|
||||
|
||||
ProxyConfigGlobal string = "global"
|
||||
)
|
||||
|
||||
type ConfigEntry interface {
|
||||
GetKind() string
|
||||
GetName() string
|
||||
GetCreateIndex() uint64
|
||||
GetModifyIndex() uint64
|
||||
}
|
||||
|
||||
type MeshGatewayMode string
|
||||
|
||||
const (
|
||||
// MeshGatewayModeDefault represents no specific mode and should
|
||||
// be used to indicate that a different layer of the configuration
|
||||
// chain should take precedence
|
||||
MeshGatewayModeDefault MeshGatewayMode = ""
|
||||
|
||||
// MeshGatewayModeNone represents that the Upstream Connect connections
|
||||
// should be direct and not flow through a mesh gateway.
|
||||
MeshGatewayModeNone MeshGatewayMode = "none"
|
||||
|
||||
// MeshGatewayModeLocal represents that the Upstrea Connect connections
|
||||
// should be made to a mesh gateway in the local datacenter. This is
|
||||
MeshGatewayModeLocal MeshGatewayMode = "local"
|
||||
|
||||
// MeshGatewayModeRemote represents that the Upstream Connect connections
|
||||
// should be made to a mesh gateway in a remote datacenter.
|
||||
MeshGatewayModeRemote MeshGatewayMode = "remote"
|
||||
)
|
||||
|
||||
// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect
|
||||
// services
|
||||
type MeshGatewayConfig struct {
|
||||
// Mode is the mode that should be used for the upstream connection.
|
||||
Mode MeshGatewayMode `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
|
||||
// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
|
||||
type ExposeConfig struct {
|
||||
// Checks defines whether paths associated with Consul checks will be exposed.
|
||||
// This flag triggers exposing all HTTP and GRPC check paths registered for the service.
|
||||
Checks bool `json:",omitempty"`
|
||||
|
||||
// Paths is the list of paths exposed through the proxy.
|
||||
Paths []ExposePath `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ExposePath struct {
|
||||
// ListenerPort defines the port of the proxy's listener for exposed paths.
|
||||
ListenerPort int `json:",omitempty" alias:"listener_port"`
|
||||
|
||||
// Path is the path to expose through the proxy, ie. "/metrics."
|
||||
Path string `json:",omitempty"`
|
||||
|
||||
// LocalPathPort is the port that the service is listening on for the given path.
|
||||
LocalPathPort int `json:",omitempty" alias:"local_path_port"`
|
||||
|
||||
// Protocol describes the upstream's service protocol.
|
||||
// Valid values are "http" and "http2", defaults to "http"
|
||||
Protocol string `json:",omitempty"`
|
||||
|
||||
// ParsedFromCheck is set if this path was parsed from a registered check
|
||||
ParsedFromCheck bool
|
||||
}
|
||||
|
||||
type ServiceConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
Protocol string `json:",omitempty"`
|
||||
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
|
||||
Expose ExposeConfig `json:",omitempty"`
|
||||
ExternalSNI string `json:",omitempty" alias:"external_sni"`
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetKind() string {
|
||||
return s.Kind
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
|
||||
return s.CreateIndex
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetModifyIndex() uint64 {
|
||||
return s.ModifyIndex
|
||||
}
|
||||
|
||||
type ProxyConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
Config map[string]interface{} `json:",omitempty"`
|
||||
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
|
||||
Expose ExposeConfig `json:",omitempty"`
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetKind() string {
|
||||
return p.Kind
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetName() string {
|
||||
return p.Name
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
|
||||
return p.CreateIndex
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetModifyIndex() uint64 {
|
||||
return p.ModifyIndex
|
||||
}
|
||||
|
||||
func makeConfigEntry(kind, name string) (ConfigEntry, error) {
|
||||
switch kind {
|
||||
case ServiceDefaults:
|
||||
return &ServiceConfigEntry{Kind: kind, Name: name}, nil
|
||||
case ProxyDefaults:
|
||||
return &ProxyConfigEntry{Kind: kind, Name: name}, nil
|
||||
case ServiceRouter:
|
||||
return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil
|
||||
case ServiceSplitter:
|
||||
return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil
|
||||
case ServiceResolver:
|
||||
return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil
|
||||
case IngressGateway:
|
||||
return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
|
||||
case TerminatingGateway:
|
||||
return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
|
||||
return makeConfigEntry(kind, name)
|
||||
}
|
||||
|
||||
// DecodeConfigEntry will decode the result of using json.Unmarshal of a config
|
||||
// entry into a map[string]interface{}.
|
||||
//
|
||||
// Important caveats:
|
||||
//
|
||||
// - This will NOT work if the map[string]interface{} was produced using HCL
|
||||
// decoding as that requires more extensive parsing to work around the issues
|
||||
// with map[string][]interface{} that arise.
|
||||
//
|
||||
// - This will only decode fields using their camel case json field
|
||||
// representations.
|
||||
func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
|
||||
var entry ConfigEntry
|
||||
|
||||
kindVal, ok := raw["Kind"]
|
||||
if !ok {
|
||||
kindVal, ok = raw["kind"]
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
|
||||
}
|
||||
|
||||
if kindStr, ok := kindVal.(string); ok {
|
||||
newEntry, err := makeConfigEntry(kindStr, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry = newEntry
|
||||
} else {
|
||||
return nil, fmt.Errorf("Kind value in payload is not a string")
|
||||
}
|
||||
|
||||
decodeConf := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
Result: &entry,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(decodeConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entry, decoder.Decode(raw)
|
||||
}
|
||||
|
||||
func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DecodeConfigEntry(raw)
|
||||
}
|
||||
|
||||
func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) {
|
||||
var entries []ConfigEntry
|
||||
for _, rawEntry := range raw {
|
||||
entry, err := DecodeConfigEntry(rawEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// ConfigEntries can be used to query the Config endpoints
|
||||
type ConfigEntries struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Config returns a handle to the Config endpoints
|
||||
func (c *Client) ConfigEntries() *ConfigEntries {
|
||||
return &ConfigEntries{c}
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) {
|
||||
if kind == "" || name == "" {
|
||||
return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
}
|
||||
|
||||
entry, err := makeConfigEntry(kind, name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if err := decodeBody(resp, entry); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return entry, qm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) {
|
||||
if kind == "" {
|
||||
return nil, nil, fmt.Errorf("The kind parameter must not be empty")
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind))
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var raw []map[string]interface{}
|
||||
if err := decodeBody(resp, &raw); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
entries, err := decodeConfigEntrySlice(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
return conf.set(entry, nil, w)
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
r := conf.c.newRequest("PUT", "/v1/config")
|
||||
r.setWriteOptions(w)
|
||||
for param, value := range params {
|
||||
r.params.Set(param, value)
|
||||
}
|
||||
r.obj = entry
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return res, wm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
|
||||
if kind == "" || name == "" {
|
||||
return nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
||||
r.setWriteOptions(w)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return wm, nil
|
||||
}
|
203
vendor/github.com/hashicorp/consul/api/config_entry_discoverychain.go
generated
vendored
Normal file
203
vendor/github.com/hashicorp/consul/api/config_entry_discoverychain.go
generated
vendored
Normal file
|
@ -0,0 +1,203 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServiceRouterConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
Routes []ServiceRoute `json:",omitempty"`
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (e *ServiceRouterConfigEntry) GetKind() string { return e.Kind }
|
||||
func (e *ServiceRouterConfigEntry) GetName() string { return e.Name }
|
||||
func (e *ServiceRouterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
|
||||
func (e *ServiceRouterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
|
||||
|
||||
type ServiceRoute struct {
|
||||
Match *ServiceRouteMatch `json:",omitempty"`
|
||||
Destination *ServiceRouteDestination `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRouteMatch struct {
|
||||
HTTP *ServiceRouteHTTPMatch `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRouteHTTPMatch struct {
|
||||
PathExact string `json:",omitempty" alias:"path_exact"`
|
||||
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
||||
PathRegex string `json:",omitempty" alias:"path_regex"`
|
||||
|
||||
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
||||
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
|
||||
Methods []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRouteHTTPMatchHeader struct {
|
||||
Name string
|
||||
Present bool `json:",omitempty"`
|
||||
Exact string `json:",omitempty"`
|
||||
Prefix string `json:",omitempty"`
|
||||
Suffix string `json:",omitempty"`
|
||||
Regex string `json:",omitempty"`
|
||||
Invert bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRouteHTTPMatchQueryParam struct {
|
||||
Name string
|
||||
Present bool `json:",omitempty"`
|
||||
Exact string `json:",omitempty"`
|
||||
Regex string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRouteDestination struct {
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
|
||||
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
|
||||
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
|
||||
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
|
||||
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
|
||||
}
|
||||
|
||||
func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
|
||||
type Alias ServiceRouteDestination
|
||||
exported := &struct {
|
||||
RequestTimeout string `json:",omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
RequestTimeout: e.RequestTimeout.String(),
|
||||
Alias: (*Alias)(e),
|
||||
}
|
||||
if e.RequestTimeout == 0 {
|
||||
exported.RequestTimeout = ""
|
||||
}
|
||||
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
|
||||
type Alias ServiceRouteDestination
|
||||
aux := &struct {
|
||||
RequestTimeout string
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(e),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if aux.RequestTimeout != "" {
|
||||
if e.RequestTimeout, err = time.ParseDuration(aux.RequestTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServiceSplitterConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
Splits []ServiceSplit `json:",omitempty"`
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (e *ServiceSplitterConfigEntry) GetKind() string { return e.Kind }
|
||||
func (e *ServiceSplitterConfigEntry) GetName() string { return e.Name }
|
||||
func (e *ServiceSplitterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
|
||||
func (e *ServiceSplitterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
|
||||
|
||||
type ServiceSplit struct {
|
||||
Weight float32
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceResolverConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
DefaultSubset string `json:",omitempty" alias:"default_subset"`
|
||||
Subsets map[string]ServiceResolverSubset `json:",omitempty"`
|
||||
Redirect *ServiceResolverRedirect `json:",omitempty"`
|
||||
Failover map[string]ServiceResolverFailover `json:",omitempty"`
|
||||
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (e *ServiceResolverConfigEntry) MarshalJSON() ([]byte, error) {
|
||||
type Alias ServiceResolverConfigEntry
|
||||
exported := &struct {
|
||||
ConnectTimeout string `json:",omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
ConnectTimeout: e.ConnectTimeout.String(),
|
||||
Alias: (*Alias)(e),
|
||||
}
|
||||
if e.ConnectTimeout == 0 {
|
||||
exported.ConnectTimeout = ""
|
||||
}
|
||||
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
func (e *ServiceResolverConfigEntry) UnmarshalJSON(data []byte) error {
|
||||
type Alias ServiceResolverConfigEntry
|
||||
aux := &struct {
|
||||
ConnectTimeout string
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(e),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if aux.ConnectTimeout != "" {
|
||||
if e.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ServiceResolverConfigEntry) GetKind() string { return e.Kind }
|
||||
func (e *ServiceResolverConfigEntry) GetName() string { return e.Name }
|
||||
func (e *ServiceResolverConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
|
||||
func (e *ServiceResolverConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
|
||||
|
||||
type ServiceResolverSubset struct {
|
||||
Filter string `json:",omitempty"`
|
||||
OnlyPassing bool `json:",omitempty" alias:"only_passing"`
|
||||
}
|
||||
|
||||
type ServiceResolverRedirect struct {
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
Datacenter string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ServiceResolverFailover struct {
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
Datacenters []string `json:",omitempty"`
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package api
|
||||
|
||||
// IngressGatewayConfigEntry manages the configuration for an ingress service
|
||||
// with the given name.
|
||||
type IngressGatewayConfigEntry struct {
|
||||
// Kind of the config entry. This should be set to api.IngressGateway.
|
||||
Kind string
|
||||
|
||||
// Name is used to match the config entry with its associated ingress gateway
|
||||
// service. This should match the name provided in the service definition.
|
||||
Name string
|
||||
|
||||
// Namespace is the namespace the IngressGateway is associated with
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// TLS holds the TLS configuration for this gateway.
|
||||
TLS GatewayTLSConfig
|
||||
|
||||
// Listeners declares what ports the ingress gateway should listen on, and
|
||||
// what services to associated to those ports.
|
||||
Listeners []IngressListener
|
||||
|
||||
// CreateIndex is the Raft index this entry was created at. This is a
|
||||
// read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type GatewayTLSConfig struct {
|
||||
// Indicates that TLS should be enabled for this gateway service
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// IngressListener manages the configuration for a listener on a specific port.
|
||||
type IngressListener struct {
|
||||
// Port declares the port on which the ingress gateway should listen for traffic.
|
||||
Port int
|
||||
|
||||
// Protocol declares what type of traffic this listener is expected to
|
||||
// receive. Depending on the protocol, a listener might support multiplexing
|
||||
// services over a single port, or additional discovery chain features. The
|
||||
// current supported values are: (tcp | http).
|
||||
Protocol string
|
||||
|
||||
// Services declares the set of services to which the listener forwards
|
||||
// traffic.
|
||||
//
|
||||
// For "tcp" protocol listeners, only a single service is allowed.
|
||||
// For "http" listeners, multiple services can be declared.
|
||||
Services []IngressService
|
||||
}
|
||||
|
||||
// IngressService manages configuration for services that are exposed to
|
||||
// ingress traffic.
|
||||
type IngressService struct {
|
||||
// Name declares the service to which traffic should be forwarded.
|
||||
//
|
||||
// This can either be a specific service, or the wildcard specifier,
|
||||
// "*". If the wildcard specifier is provided, the listener must be of "http"
|
||||
// protocol and means that the listener will forward traffic to all services.
|
||||
//
|
||||
// A name can be specified on multiple listeners, and will be exposed on both
|
||||
// of the listeners
|
||||
Name string
|
||||
|
||||
// Hosts is a list of hostnames which should be associated to this service on
|
||||
// the defined listener. Only allowed on layer 7 protocols, this will be used
|
||||
// to route traffic to the service by matching the Host header of the HTTP
|
||||
// request.
|
||||
//
|
||||
// If a host is provided for a service that also has a wildcard specifier
|
||||
// defined, the host will override the wildcard-specifier-provided
|
||||
// "<service-name>.*" domain for that listener.
|
||||
//
|
||||
// This cannot be specified when using the wildcard specifier, "*", or when
|
||||
// using a "tcp" listener.
|
||||
Hosts []string
|
||||
|
||||
// Namespace is the namespace where the service is located.
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetKind() string {
|
||||
return i.Kind
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetCreateIndex() uint64 {
|
||||
return i.CreateIndex
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetModifyIndex() uint64 {
|
||||
return i.ModifyIndex
|
||||
}
|
||||
|
||||
// TerminatingGatewayConfigEntry manages the configuration for a terminating gateway
|
||||
// with the given name.
|
||||
type TerminatingGatewayConfigEntry struct {
|
||||
// Kind of the config entry. This should be set to api.TerminatingGateway.
|
||||
Kind string
|
||||
|
||||
// Name is used to match the config entry with its associated terminating gateway
|
||||
// service. This should match the name provided in the service definition.
|
||||
Name string
|
||||
|
||||
// Services is a list of service names represented by the terminating gateway.
|
||||
Services []LinkedService `json:",omitempty"`
|
||||
|
||||
// CreateIndex is the Raft index this entry was created at. This is a
|
||||
// read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64
|
||||
|
||||
// Namespace is the namespace the config entry is associated with
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// A LinkedService is a service represented by a terminating gateway
|
||||
type LinkedService struct {
|
||||
// The namespace the service is registered in
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// Name is the name of the service, as defined in Consul's catalog
|
||||
Name string `json:",omitempty"`
|
||||
|
||||
// CAFile is the optional path to a CA certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CAFile string `json:",omitempty" alias:"ca_file"`
|
||||
|
||||
// CertFile is the optional path to a client certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CertFile string `json:",omitempty" alias:"cert_file"`
|
||||
|
||||
// KeyFile is the optional path to a private key to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
KeyFile string `json:",omitempty" alias:"key_file"`
|
||||
|
||||
// SNI is the optional name to specify during the TLS handshake with a linked service
|
||||
SNI string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (g *TerminatingGatewayConfigEntry) GetKind() string {
|
||||
return g.Kind
|
||||
}
|
||||
|
||||
func (g *TerminatingGatewayConfigEntry) GetName() string {
|
||||
return g.Name
|
||||
}
|
||||
|
||||
func (g *TerminatingGatewayConfigEntry) GetCreateIndex() uint64 {
|
||||
return g.CreateIndex
|
||||
}
|
||||
|
||||
func (g *TerminatingGatewayConfigEntry) GetModifyIndex() uint64 {
|
||||
return g.ModifyIndex
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package api
|
||||
|
||||
// Connect can be used to work with endpoints related to Connect, the
|
||||
// feature for securely connecting services within Consul.
|
||||
type Connect struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Connect returns a handle to the connect-related endpoints
|
||||
func (c *Client) Connect() *Connect {
|
||||
return &Connect{c}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// CAConfig is the structure for the Connect CA configuration.
|
||||
type CAConfig struct {
|
||||
// Provider is the CA provider implementation to use.
|
||||
Provider string
|
||||
|
||||
// Configuration is arbitrary configuration for the provider. This
|
||||
// should only contain primitive values and containers (such as lists
|
||||
// and maps).
|
||||
Config map[string]interface{}
|
||||
|
||||
// State is read-only data that the provider might have persisted for use
|
||||
// after restart or leadership transition. For example this might include
|
||||
// UUIDs of resources it has created. Setting this when writing a
|
||||
// configuration is an error.
|
||||
State map[string]string
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// CommonCAProviderConfig is the common options available to all CA providers.
|
||||
type CommonCAProviderConfig struct {
|
||||
LeafCertTTL time.Duration
|
||||
SkipValidate bool
|
||||
CSRMaxPerSecond float32
|
||||
CSRMaxConcurrent int
|
||||
}
|
||||
|
||||
// ConsulCAProviderConfig is the config for the built-in Consul CA provider.
|
||||
type ConsulCAProviderConfig struct {
|
||||
CommonCAProviderConfig `mapstructure:",squash"`
|
||||
|
||||
PrivateKey string
|
||||
RootCert string
|
||||
RotationPeriod time.Duration
|
||||
IntermediateCertTTL time.Duration
|
||||
}
|
||||
|
||||
// ParseConsulCAConfig takes a raw config map and returns a parsed
|
||||
// ConsulCAProviderConfig.
|
||||
func ParseConsulCAConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) {
|
||||
var config ConsulCAProviderConfig
|
||||
decodeConf := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
Result: &config,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(decodeConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(raw); err != nil {
|
||||
return nil, fmt.Errorf("error decoding config: %s", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// CARootList is the structure for the results of listing roots.
|
||||
type CARootList struct {
|
||||
ActiveRootID string
|
||||
TrustDomain string
|
||||
Roots []*CARoot
|
||||
}
|
||||
|
||||
// CARoot represents a root CA certificate that is trusted.
|
||||
type CARoot struct {
|
||||
// ID is a globally unique ID (UUID) representing this CA root.
|
||||
ID string
|
||||
|
||||
// Name is a human-friendly name for this CA root. This value is
|
||||
// opaque to Consul and is not used for anything internally.
|
||||
Name string
|
||||
|
||||
// RootCertPEM is the PEM-encoded public certificate.
|
||||
RootCertPEM string `json:"RootCert"`
|
||||
|
||||
// Active is true if this is the current active CA. This must only
|
||||
// be true for exactly one CA. For any method that modifies roots in the
|
||||
// state store, tests should be written to verify that multiple roots
|
||||
// cannot be active.
|
||||
Active bool
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// LeafCert is a certificate that has been issued by a Connect CA.
|
||||
type LeafCert struct {
|
||||
// SerialNumber is the unique serial number for this certificate.
|
||||
// This is encoded in standard hex separated by :.
|
||||
SerialNumber string
|
||||
|
||||
// CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private
|
||||
// key for that cert, respectively. This should not be stored in the
|
||||
// state store, but is present in the sign API response.
|
||||
CertPEM string `json:",omitempty"`
|
||||
PrivateKeyPEM string `json:",omitempty"`
|
||||
|
||||
// Service is the name of the service for which the cert was issued.
|
||||
// ServiceURI is the cert URI value.
|
||||
Service string
|
||||
ServiceURI string
|
||||
|
||||
// ValidAfter and ValidBefore are the validity periods for the
|
||||
// certificate.
|
||||
ValidAfter time.Time
|
||||
ValidBefore time.Time
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// CARoots queries the list of available roots.
|
||||
func (h *Connect) CARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/ca/roots")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out CARootList
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
// CAGetConfig returns the current CA configuration.
|
||||
func (h *Connect) CAGetConfig(q *QueryOptions) (*CAConfig, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/ca/configuration")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out CAConfig
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
// CASetConfig sets the current CA configuration.
|
||||
func (h *Connect) CASetConfig(conf *CAConfig, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := h.c.newRequest("PUT", "/v1/connect/ca/configuration")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = conf
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
return wm, nil
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Intention defines an intention for the Connect Service Graph. This defines
|
||||
// the allowed or denied behavior of a connection between two services using
|
||||
// Connect.
|
||||
type Intention struct {
|
||||
// ID is the UUID-based ID for the intention, always generated by Consul.
|
||||
ID string
|
||||
|
||||
// Description is a human-friendly description of this intention.
|
||||
// It is opaque to Consul and is only stored and transferred in API
|
||||
// requests.
|
||||
Description string
|
||||
|
||||
// SourceNS, SourceName are the namespace and name, respectively, of
|
||||
// the source service. Either of these may be the wildcard "*", but only
|
||||
// the full value can be a wildcard. Partial wildcards are not allowed.
|
||||
// The source may also be a non-Consul service, as specified by SourceType.
|
||||
//
|
||||
// DestinationNS, DestinationName is the same, but for the destination
|
||||
// service. The same rules apply. The destination is always a Consul
|
||||
// service.
|
||||
SourceNS, SourceName string
|
||||
DestinationNS, DestinationName string
|
||||
|
||||
// SourceType is the type of the value for the source.
|
||||
SourceType IntentionSourceType
|
||||
|
||||
// Action is whether this is an allowlist or denylist intention.
|
||||
Action IntentionAction
|
||||
|
||||
// DefaultAddr is not used.
|
||||
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
||||
DefaultAddr string `json:",omitempty"`
|
||||
// DefaultPort is not used.
|
||||
// Deprecated: DefaultPort is not used and may be removed in a future version.
|
||||
DefaultPort int `json:",omitempty"`
|
||||
|
||||
// Meta is arbitrary metadata associated with the intention. This is
|
||||
// opaque to Consul but is served in API responses.
|
||||
Meta map[string]string
|
||||
|
||||
// Precedence is the order that the intention will be applied, with
|
||||
// larger numbers being applied first. This is a read-only field, on
|
||||
// any intention update it is updated.
|
||||
Precedence int
|
||||
|
||||
// CreatedAt and UpdatedAt keep track of when this record was created
|
||||
// or modified.
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
|
||||
// Hash of the contents of the intention
|
||||
//
|
||||
// This is needed mainly for replication purposes. When replicating from
|
||||
// one DC to another keeping the content Hash will allow us to detect
|
||||
// content changes more efficiently than checking every single field
|
||||
Hash []byte
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// String returns human-friendly output describing ths intention.
|
||||
func (i *Intention) String() string {
|
||||
return fmt.Sprintf("%s => %s (%s)",
|
||||
i.SourceString(),
|
||||
i.DestinationString(),
|
||||
i.Action)
|
||||
}
|
||||
|
||||
// SourceString returns the namespace/name format for the source, or
|
||||
// just "name" if the namespace is the default namespace.
|
||||
func (i *Intention) SourceString() string {
|
||||
return i.partString(i.SourceNS, i.SourceName)
|
||||
}
|
||||
|
||||
// DestinationString returns the namespace/name format for the source, or
|
||||
// just "name" if the namespace is the default namespace.
|
||||
func (i *Intention) DestinationString() string {
|
||||
return i.partString(i.DestinationNS, i.DestinationName)
|
||||
}
|
||||
|
||||
func (i *Intention) partString(ns, n string) string {
|
||||
// For now we omit the default namespace from the output. In the future
|
||||
// we might want to look at this and show this in a multi-namespace world.
|
||||
if ns != "" && ns != IntentionDefaultNamespace {
|
||||
n = ns + "/" + n
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// IntentionDefaultNamespace is the default namespace value.
|
||||
const IntentionDefaultNamespace = "default"
|
||||
|
||||
// IntentionAction is the action that the intention represents. This
|
||||
// can be "allow" or "deny" to allowlist or denylist intentions.
|
||||
type IntentionAction string
|
||||
|
||||
const (
|
||||
IntentionActionAllow IntentionAction = "allow"
|
||||
IntentionActionDeny IntentionAction = "deny"
|
||||
)
|
||||
|
||||
// IntentionSourceType is the type of the source within an intention.
|
||||
type IntentionSourceType string
|
||||
|
||||
const (
|
||||
// IntentionSourceConsul is a service within the Consul catalog.
|
||||
IntentionSourceConsul IntentionSourceType = "consul"
|
||||
)
|
||||
|
||||
// IntentionMatch are the arguments for the intention match API.
|
||||
type IntentionMatch struct {
|
||||
By IntentionMatchType
|
||||
Names []string
|
||||
}
|
||||
|
||||
// IntentionMatchType is the target for a match request. For example,
|
||||
// matching by source will look for all intentions that match the given
|
||||
// source value.
|
||||
type IntentionMatchType string
|
||||
|
||||
const (
|
||||
IntentionMatchSource IntentionMatchType = "source"
|
||||
IntentionMatchDestination IntentionMatchType = "destination"
|
||||
)
|
||||
|
||||
// IntentionCheck are the arguments for the intention check API. For
|
||||
// more documentation see the IntentionCheck function.
|
||||
type IntentionCheck struct {
|
||||
// Source and Destination are the source and destination values to
|
||||
// check. The destination is always a Consul service, but the source
|
||||
// may be other values as defined by the SourceType.
|
||||
Source, Destination string
|
||||
|
||||
// SourceType is the type of the value for the source.
|
||||
SourceType IntentionSourceType
|
||||
}
|
||||
|
||||
// Intentions returns the list of intentions.
|
||||
func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/intentions")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*Intention
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// IntentionGet retrieves a single intention.
|
||||
func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/intentions/"+id)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := h.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, qm, nil
|
||||
} else if resp.StatusCode != 200 {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
return nil, nil, fmt.Errorf(
|
||||
"Unexpected response %d: %s", resp.StatusCode, buf.String())
|
||||
}
|
||||
|
||||
var out Intention
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
// IntentionDelete deletes a single intention.
|
||||
func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id)
|
||||
r.setWriteOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &WriteMeta{}
|
||||
qm.RequestTime = rtt
|
||||
|
||||
return qm, nil
|
||||
}
|
||||
|
||||
// IntentionMatch returns the list of intentions that match a given source
|
||||
// or destination. The returned intentions are ordered by precedence where
|
||||
// result[0] is the highest precedence (if that matches, then that rule overrides
|
||||
// all other rules).
|
||||
//
|
||||
// Matching can be done for multiple names at the same time. The resulting
|
||||
// map is keyed by the given names. Casing is preserved.
|
||||
func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/intentions/match")
|
||||
r.setQueryOptions(q)
|
||||
r.params.Set("by", string(args.By))
|
||||
for _, name := range args.Names {
|
||||
r.params.Add("name", name)
|
||||
}
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out map[string][]*Intention
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// IntentionCheck returns whether a given source/destination would be allowed
|
||||
// or not given the current set of intentions and the configuration of Consul.
|
||||
func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/intentions/check")
|
||||
r.setQueryOptions(q)
|
||||
r.params.Set("source", args.Source)
|
||||
r.params.Set("destination", args.Destination)
|
||||
if args.SourceType != "" {
|
||||
r.params.Set("source-type", string(args.SourceType))
|
||||
}
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out struct{ Allowed bool }
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return out.Allowed, qm, nil
|
||||
}
|
||||
|
||||
// IntentionGetExact retrieves a single intention by its unique name instead of
|
||||
// its ID.
|
||||
func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/connect/intentions/exact")
|
||||
r.setQueryOptions(q)
|
||||
r.params.Set("source", source)
|
||||
r.params.Set("destination", destination)
|
||||
rtt, resp, err := h.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, qm, nil
|
||||
} else if resp.StatusCode != 200 {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
return nil, nil, fmt.Errorf(
|
||||
"Unexpected response %d: %s", resp.StatusCode, buf.String())
|
||||
}
|
||||
|
||||
var out Intention
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
// IntentionCreate will create a new intention. The ID in the given
|
||||
// structure must be empty and a generate ID will be returned on
|
||||
// success.
|
||||
func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
r := c.c.newRequest("POST", "/v1/connect/intentions")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = ixn
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
var out struct{ ID string }
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// IntentionUpdate will update an existing intention. The ID in the given
|
||||
// structure must be non-empty.
|
||||
func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID)
|
||||
r.setWriteOptions(q)
|
||||
r.obj = ixn
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
return wm, nil
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/serf/coordinate"
|
||||
)
|
||||
|
||||
// CoordinateEntry represents a node and its associated network coordinate.
|
||||
type CoordinateEntry struct {
|
||||
Node string
|
||||
Segment string
|
||||
Coord *coordinate.Coordinate
|
||||
}
|
||||
|
||||
// CoordinateDatacenterMap has the coordinates for servers in a given datacenter
|
||||
// and area. Network coordinates are only compatible within the same area.
|
||||
type CoordinateDatacenterMap struct {
|
||||
Datacenter string
|
||||
AreaID string
|
||||
Coordinates []CoordinateEntry
|
||||
}
|
||||
|
||||
// Coordinate can be used to query the coordinate endpoints
|
||||
type Coordinate struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Coordinate returns a handle to the coordinate endpoints
|
||||
func (c *Client) Coordinate() *Coordinate {
|
||||
return &Coordinate{c}
|
||||
}
|
||||
|
||||
// Datacenters is used to return the coordinates of all the servers in the WAN
|
||||
// pool.
|
||||
func (c *Coordinate) Datacenters() ([]*CoordinateDatacenterMap, error) {
|
||||
r := c.c.newRequest("GET", "/v1/coordinate/datacenters")
|
||||
_, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out []*CoordinateDatacenterMap
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Nodes is used to return the coordinates of all the nodes in the LAN pool.
|
||||
func (c *Coordinate) Nodes(q *QueryOptions) ([]*CoordinateEntry, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/coordinate/nodes")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*CoordinateEntry
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Update inserts or updates the LAN coordinate of a node.
|
||||
func (c *Coordinate) Update(coord *CoordinateEntry, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := c.c.newRequest("PUT", "/v1/coordinate/update")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = coord
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Node is used to return the coordinates of a single node in the LAN pool.
|
||||
func (c *Coordinate) Node(node string, q *QueryOptions) ([]*CoordinateEntry, *QueryMeta, error) {
|
||||
r := c.c.newRequest("GET", "/v1/coordinate/node/"+node)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*CoordinateEntry
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Debug can be used to query the /debug/pprof endpoints to gather
|
||||
// profiling information about the target agent.Debug
|
||||
//
|
||||
// The agent must have enable_debug set to true for profiling to be enabled
|
||||
// and for these endpoints to function.
|
||||
type Debug struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Debug returns a handle that exposes the internal debug endpoints.
|
||||
func (c *Client) Debug() *Debug {
|
||||
return &Debug{c}
|
||||
}
|
||||
|
||||
// Heap returns a pprof heap dump
|
||||
func (d *Debug) Heap() ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/heap")
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Profile returns a pprof CPU profile for the specified number of seconds
|
||||
func (d *Debug) Profile(seconds int) ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/profile")
|
||||
|
||||
// Capture a profile for the specified number of seconds
|
||||
r.params.Set("seconds", strconv.Itoa(seconds))
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Trace returns an execution trace
|
||||
func (d *Debug) Trace(seconds int) ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/trace")
|
||||
|
||||
// Capture a trace for the specified number of seconds
|
||||
r.params.Set("seconds", strconv.Itoa(seconds))
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Goroutine returns a pprof goroutine profile
|
||||
func (d *Debug) Goroutine() ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/goroutine")
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DiscoveryChain can be used to query the discovery-chain endpoints
|
||||
type DiscoveryChain struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// DiscoveryChain returns a handle to the discovery-chain endpoints
|
||||
func (c *Client) DiscoveryChain() *DiscoveryChain {
|
||||
return &DiscoveryChain{c}
|
||||
}
|
||||
|
||||
func (d *DiscoveryChain) Get(name string, opts *DiscoveryChainOptions, q *QueryOptions) (*DiscoveryChainResponse, *QueryMeta, error) {
|
||||
if name == "" {
|
||||
return nil, nil, fmt.Errorf("Name parameter must not be empty")
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
if opts != nil && opts.requiresPOST() {
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
r := d.c.newRequest(method, fmt.Sprintf("/v1/discovery-chain/%s", name))
|
||||
r.setQueryOptions(q)
|
||||
|
||||
if opts != nil {
|
||||
if opts.EvaluateInDatacenter != "" {
|
||||
r.params.Set("compile-dc", opts.EvaluateInDatacenter)
|
||||
}
|
||||
}
|
||||
|
||||
if method == "POST" {
|
||||
r.obj = opts
|
||||
}
|
||||
|
||||
rtt, resp, err := requireOK(d.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out DiscoveryChainResponse
|
||||
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
type DiscoveryChainOptions struct {
|
||||
EvaluateInDatacenter string `json:"-"`
|
||||
|
||||
// OverrideMeshGateway allows for the mesh gateway setting to be overridden
|
||||
// for any resolver in the compiled chain.
|
||||
OverrideMeshGateway MeshGatewayConfig `json:",omitempty"`
|
||||
|
||||
// OverrideProtocol allows for the final protocol for the chain to be
|
||||
// altered.
|
||||
//
|
||||
// - If the chain ordinarily would be TCP and an L7 protocol is passed here
|
||||
// the chain will not include Routers or Splitters.
|
||||
//
|
||||
// - If the chain ordinarily would be L7 and TCP is passed here the chain
|
||||
// will not include Routers or Splitters.
|
||||
OverrideProtocol string `json:",omitempty"`
|
||||
|
||||
// OverrideConnectTimeout allows for the ConnectTimeout setting to be
|
||||
// overridden for any resolver in the compiled chain.
|
||||
OverrideConnectTimeout time.Duration `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (o *DiscoveryChainOptions) requiresPOST() bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
return o.OverrideMeshGateway.Mode != "" ||
|
||||
o.OverrideProtocol != "" ||
|
||||
o.OverrideConnectTimeout != 0
|
||||
}
|
||||
|
||||
type DiscoveryChainResponse struct {
|
||||
Chain *CompiledDiscoveryChain
|
||||
}
|
||||
|
||||
type CompiledDiscoveryChain struct {
|
||||
ServiceName string
|
||||
Namespace string
|
||||
Datacenter string
|
||||
|
||||
// CustomizationHash is a unique hash of any data that affects the
|
||||
// compilation of the discovery chain other than config entries or the
|
||||
// name/namespace/datacenter evaluation criteria.
|
||||
//
|
||||
// If set, this value should be used to prefix/suffix any generated load
|
||||
// balancer data plane objects to avoid sharing customized and
|
||||
// non-customized versions.
|
||||
CustomizationHash string
|
||||
|
||||
// Protocol is the overall protocol shared by everything in the chain.
|
||||
Protocol string
|
||||
|
||||
// StartNode is the first key into the Nodes map that should be followed
|
||||
// when walking the discovery chain.
|
||||
StartNode string
|
||||
|
||||
// Nodes contains all nodes available for traversal in the chain keyed by a
|
||||
// unique name. You can walk this by starting with StartNode.
|
||||
//
|
||||
// NOTE: The names should be treated as opaque values and are only
|
||||
// guaranteed to be consistent within a single compilation.
|
||||
Nodes map[string]*DiscoveryGraphNode
|
||||
|
||||
// Targets is a list of all targets used in this chain.
|
||||
//
|
||||
// NOTE: The names should be treated as opaque values and are only
|
||||
// guaranteed to be consistent within a single compilation.
|
||||
Targets map[string]*DiscoveryTarget
|
||||
}
|
||||
|
||||
const (
|
||||
DiscoveryGraphNodeTypeRouter = "router"
|
||||
DiscoveryGraphNodeTypeSplitter = "splitter"
|
||||
DiscoveryGraphNodeTypeResolver = "resolver"
|
||||
)
|
||||
|
||||
// DiscoveryGraphNode is a single node in the compiled discovery chain.
|
||||
type DiscoveryGraphNode struct {
|
||||
Type string
|
||||
Name string // this is NOT necessarily a service
|
||||
|
||||
// fields for Type==router
|
||||
Routes []*DiscoveryRoute
|
||||
|
||||
// fields for Type==splitter
|
||||
Splits []*DiscoverySplit
|
||||
|
||||
// fields for Type==resolver
|
||||
Resolver *DiscoveryResolver
|
||||
}
|
||||
|
||||
// compiled form of ServiceRoute
|
||||
type DiscoveryRoute struct {
|
||||
Definition *ServiceRoute
|
||||
NextNode string
|
||||
}
|
||||
|
||||
// compiled form of ServiceSplit
|
||||
type DiscoverySplit struct {
|
||||
Weight float32
|
||||
NextNode string
|
||||
}
|
||||
|
||||
// compiled form of ServiceResolverConfigEntry
|
||||
type DiscoveryResolver struct {
|
||||
Default bool
|
||||
ConnectTimeout time.Duration
|
||||
Target string
|
||||
Failover *DiscoveryFailover
|
||||
}
|
||||
|
||||
func (r *DiscoveryResolver) MarshalJSON() ([]byte, error) {
|
||||
type Alias DiscoveryResolver
|
||||
exported := &struct {
|
||||
ConnectTimeout string `json:",omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
ConnectTimeout: r.ConnectTimeout.String(),
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if r.ConnectTimeout == 0 {
|
||||
exported.ConnectTimeout = ""
|
||||
}
|
||||
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
func (r *DiscoveryResolver) UnmarshalJSON(data []byte) error {
|
||||
type Alias DiscoveryResolver
|
||||
aux := &struct {
|
||||
ConnectTimeout string
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if aux.ConnectTimeout != "" {
|
||||
if r.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compiled form of ServiceResolverFailover
|
||||
type DiscoveryFailover struct {
|
||||
Targets []string
|
||||
}
|
||||
|
||||
// DiscoveryTarget represents all of the inputs necessary to use a resolver
|
||||
// config entry to execute a catalog query to generate a list of service
|
||||
// instances during discovery.
|
||||
type DiscoveryTarget struct {
|
||||
ID string
|
||||
|
||||
Service string
|
||||
ServiceSubset string
|
||||
Namespace string
|
||||
Datacenter string
|
||||
|
||||
MeshGateway MeshGatewayConfig
|
||||
Subset ServiceResolverSubset
|
||||
External bool
|
||||
SNI string
|
||||
Name string
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Event can be used to query the Event endpoints
|
||||
type Event struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// UserEvent represents an event that was fired by the user
|
||||
type UserEvent struct {
|
||||
ID string
|
||||
Name string
|
||||
Payload []byte
|
||||
NodeFilter string
|
||||
ServiceFilter string
|
||||
TagFilter string
|
||||
Version int
|
||||
LTime uint64
|
||||
}
|
||||
|
||||
// Event returns a handle to the event endpoints
|
||||
func (c *Client) Event() *Event {
|
||||
return &Event{c}
|
||||
}
|
||||
|
||||
// Fire is used to fire a new user event. Only the Name, Payload and Filters
|
||||
// are respected. This returns the ID or an associated error. Cross DC requests
|
||||
// are supported.
|
||||
func (e *Event) Fire(params *UserEvent, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
r := e.c.newRequest("PUT", "/v1/event/fire/"+params.Name)
|
||||
r.setWriteOptions(q)
|
||||
if params.NodeFilter != "" {
|
||||
r.params.Set("node", params.NodeFilter)
|
||||
}
|
||||
if params.ServiceFilter != "" {
|
||||
r.params.Set("service", params.ServiceFilter)
|
||||
}
|
||||
if params.TagFilter != "" {
|
||||
r.params.Set("tag", params.TagFilter)
|
||||
}
|
||||
if params.Payload != nil {
|
||||
r.body = bytes.NewReader(params.Payload)
|
||||
}
|
||||
|
||||
rtt, resp, err := requireOK(e.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
var out UserEvent
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// List is used to get the most recent events an agent has received.
|
||||
// This list can be optionally filtered by the name. This endpoint supports
|
||||
// quasi-blocking queries. The index is not monotonic, nor does it provide provide
|
||||
// LastContact or KnownLeader.
|
||||
func (e *Event) List(name string, q *QueryOptions) ([]*UserEvent, *QueryMeta, error) {
|
||||
r := e.c.newRequest("GET", "/v1/event/list")
|
||||
r.setQueryOptions(q)
|
||||
if name != "" {
|
||||
r.params.Set("name", name)
|
||||
}
|
||||
rtt, resp, err := requireOK(e.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var entries []*UserEvent
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
// IDToIndex is a bit of a hack. This simulates the index generation to
|
||||
// convert an event ID into a WaitIndex.
|
||||
func (e *Event) IDToIndex(uuid string) uint64 {
|
||||
lower := uuid[0:8] + uuid[9:13] + uuid[14:18]
|
||||
upper := uuid[19:23] + uuid[24:36]
|
||||
lowVal, err := strconv.ParseUint(lower, 16, 64)
|
||||
if err != nil {
|
||||
panic("Failed to convert " + lower)
|
||||
}
|
||||
highVal, err := strconv.ParseUint(upper, 16, 64)
|
||||
if err != nil {
|
||||
panic("Failed to convert " + upper)
|
||||
}
|
||||
return lowVal ^ highVal
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
module github.com/hashicorp/consul/api
|
||||
|
||||
go 1.12
|
||||
|
||||
replace github.com/hashicorp/consul/sdk => ../sdk
|
||||
|
||||
require (
|
||||
github.com/hashicorp/consul/sdk v0.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1
|
||||
github.com/hashicorp/go-hclog v0.12.0
|
||||
github.com/hashicorp/go-rootcerts v1.0.2
|
||||
github.com/hashicorp/go-uuid v1.0.1
|
||||
github.com/hashicorp/serf v0.9.3
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
)
|
|
@ -0,0 +1,131 @@
|
|||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g=
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM=
|
||||
github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
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.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,375 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// HealthAny is special, and is used as a wild card,
|
||||
// not as a specific state.
|
||||
HealthAny = "any"
|
||||
HealthPassing = "passing"
|
||||
HealthWarning = "warning"
|
||||
HealthCritical = "critical"
|
||||
HealthMaint = "maintenance"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceHealth = "service"
|
||||
connectHealth = "connect"
|
||||
ingressHealth = "ingress"
|
||||
)
|
||||
|
||||
const (
|
||||
// NodeMaint is the special key set by a node in maintenance mode.
|
||||
NodeMaint = "_node_maintenance"
|
||||
|
||||
// ServiceMaintPrefix is the prefix for a service in maintenance mode.
|
||||
ServiceMaintPrefix = "_service_maintenance:"
|
||||
)
|
||||
|
||||
// HealthCheck is used to represent a single check
|
||||
type HealthCheck struct {
|
||||
Node string
|
||||
CheckID string
|
||||
Name string
|
||||
Status string
|
||||
Notes string
|
||||
Output string
|
||||
ServiceID string
|
||||
ServiceName string
|
||||
ServiceTags []string
|
||||
Type string
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
Definition HealthCheckDefinition
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// HealthCheckDefinition is used to store the details about
|
||||
// a health check's execution.
|
||||
type HealthCheckDefinition struct {
|
||||
HTTP string
|
||||
Header map[string][]string
|
||||
Method string
|
||||
Body string
|
||||
TLSSkipVerify bool
|
||||
TCP string
|
||||
IntervalDuration time.Duration `json:"-"`
|
||||
TimeoutDuration time.Duration `json:"-"`
|
||||
DeregisterCriticalServiceAfterDuration time.Duration `json:"-"`
|
||||
|
||||
// DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead.
|
||||
Interval ReadableDuration
|
||||
Timeout ReadableDuration
|
||||
DeregisterCriticalServiceAfter ReadableDuration
|
||||
}
|
||||
|
||||
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
|
||||
type Alias HealthCheckDefinition
|
||||
out := &struct {
|
||||
Interval string
|
||||
Timeout string
|
||||
DeregisterCriticalServiceAfter string
|
||||
*Alias
|
||||
}{
|
||||
Interval: d.Interval.String(),
|
||||
Timeout: d.Timeout.String(),
|
||||
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
|
||||
Alias: (*Alias)(d),
|
||||
}
|
||||
|
||||
if d.IntervalDuration != 0 {
|
||||
out.Interval = d.IntervalDuration.String()
|
||||
} else if d.Interval != 0 {
|
||||
out.Interval = d.Interval.String()
|
||||
}
|
||||
if d.TimeoutDuration != 0 {
|
||||
out.Timeout = d.TimeoutDuration.String()
|
||||
} else if d.Timeout != 0 {
|
||||
out.Timeout = d.Timeout.String()
|
||||
}
|
||||
if d.DeregisterCriticalServiceAfterDuration != 0 {
|
||||
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String()
|
||||
} else if d.DeregisterCriticalServiceAfter != 0 {
|
||||
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String()
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func (t *HealthCheckDefinition) UnmarshalJSON(data []byte) (err error) {
|
||||
type Alias HealthCheckDefinition
|
||||
aux := &struct {
|
||||
IntervalDuration interface{}
|
||||
TimeoutDuration interface{}
|
||||
DeregisterCriticalServiceAfterDuration interface{}
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(t),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the values into both the time.Duration and old ReadableDuration fields.
|
||||
|
||||
if aux.IntervalDuration == nil {
|
||||
t.IntervalDuration = time.Duration(t.Interval)
|
||||
} else {
|
||||
switch v := aux.IntervalDuration.(type) {
|
||||
case string:
|
||||
if t.IntervalDuration, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
t.IntervalDuration = time.Duration(v)
|
||||
}
|
||||
t.Interval = ReadableDuration(t.IntervalDuration)
|
||||
}
|
||||
|
||||
if aux.TimeoutDuration == nil {
|
||||
t.TimeoutDuration = time.Duration(t.Timeout)
|
||||
} else {
|
||||
switch v := aux.TimeoutDuration.(type) {
|
||||
case string:
|
||||
if t.TimeoutDuration, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
t.TimeoutDuration = time.Duration(v)
|
||||
}
|
||||
t.Timeout = ReadableDuration(t.TimeoutDuration)
|
||||
}
|
||||
if aux.DeregisterCriticalServiceAfterDuration == nil {
|
||||
t.DeregisterCriticalServiceAfterDuration = time.Duration(t.DeregisterCriticalServiceAfter)
|
||||
} else {
|
||||
switch v := aux.DeregisterCriticalServiceAfterDuration.(type) {
|
||||
case string:
|
||||
if t.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
t.DeregisterCriticalServiceAfterDuration = time.Duration(v)
|
||||
}
|
||||
t.DeregisterCriticalServiceAfter = ReadableDuration(t.DeregisterCriticalServiceAfterDuration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthChecks is a collection of HealthCheck structs.
|
||||
type HealthChecks []*HealthCheck
|
||||
|
||||
// AggregatedStatus returns the "best" status for the list of health checks.
|
||||
// Because a given entry may have many service and node-level health checks
|
||||
// attached, this function determines the best representative of the status as
|
||||
// as single string using the following heuristic:
|
||||
//
|
||||
// maintenance > critical > warning > passing
|
||||
//
|
||||
func (c HealthChecks) AggregatedStatus() string {
|
||||
var passing, warning, critical, maintenance bool
|
||||
for _, check := range c {
|
||||
id := check.CheckID
|
||||
if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) {
|
||||
maintenance = true
|
||||
continue
|
||||
}
|
||||
|
||||
switch check.Status {
|
||||
case HealthPassing:
|
||||
passing = true
|
||||
case HealthWarning:
|
||||
warning = true
|
||||
case HealthCritical:
|
||||
critical = true
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case maintenance:
|
||||
return HealthMaint
|
||||
case critical:
|
||||
return HealthCritical
|
||||
case warning:
|
||||
return HealthWarning
|
||||
case passing:
|
||||
return HealthPassing
|
||||
default:
|
||||
return HealthPassing
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceEntry is used for the health service endpoint
|
||||
type ServiceEntry struct {
|
||||
Node *Node
|
||||
Service *AgentService
|
||||
Checks HealthChecks
|
||||
}
|
||||
|
||||
// Health can be used to query the Health endpoints
|
||||
type Health struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Health returns a handle to the health endpoints
|
||||
func (c *Client) Health() *Health {
|
||||
return &Health{c}
|
||||
}
|
||||
|
||||
// Node is used to query for checks belonging to a given node
|
||||
func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/health/node/"+node)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out HealthChecks
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Checks is used to return the checks associated with a service
|
||||
func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
||||
r := h.c.newRequest("GET", "/v1/health/checks/"+service)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out HealthChecks
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Service is used to query health information along with service info
|
||||
// for a given service. It can optionally do server-side filtering on a tag
|
||||
// or nodes with passing health checks only.
|
||||
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return h.service(service, tags, passingOnly, q, serviceHealth)
|
||||
}
|
||||
|
||||
func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tags, passingOnly, q, serviceHealth)
|
||||
}
|
||||
|
||||
// Connect is equivalent to Service except that it will only return services
|
||||
// which are Connect-enabled and will returns the connection address for Connect
|
||||
// client's to use which may be a proxy in front of the named service. If
|
||||
// passingOnly is true only instances where both the service and any proxy are
|
||||
// healthy will be returned.
|
||||
func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return h.service(service, tags, passingOnly, q, connectHealth)
|
||||
}
|
||||
|
||||
func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tags, passingOnly, q, connectHealth)
|
||||
}
|
||||
|
||||
// Ingress is equivalent to Connect except that it will only return associated
|
||||
// ingress gateways for the requested service.
|
||||
func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
var tags []string
|
||||
return h.service(service, tags, passingOnly, q, ingressHealth)
|
||||
}
|
||||
|
||||
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, healthType string) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
var path string
|
||||
switch healthType {
|
||||
case connectHealth:
|
||||
path = "/v1/health/connect/" + service
|
||||
case ingressHealth:
|
||||
path = "/v1/health/ingress/" + service
|
||||
default:
|
||||
path = "/v1/health/service/" + service
|
||||
}
|
||||
|
||||
r := h.c.newRequest("GET", path)
|
||||
r.setQueryOptions(q)
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
r.params.Add("tag", tag)
|
||||
}
|
||||
}
|
||||
if passingOnly {
|
||||
r.params.Set(HealthPassing, "1")
|
||||
}
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out []*ServiceEntry
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// State is used to retrieve all the checks in a given state.
|
||||
// The wildcard "any" state can also be used for all checks.
|
||||
func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
||||
switch state {
|
||||
case HealthAny:
|
||||
case HealthWarning:
|
||||
case HealthCritical:
|
||||
case HealthPassing:
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("Unsupported state: %v", state)
|
||||
}
|
||||
r := h.c.newRequest("GET", "/v1/health/state/"+state)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(h.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out HealthChecks
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KVPair is used to represent a single K/V entry
|
||||
type KVPair struct {
|
||||
// Key is the name of the key. It is also part of the URL path when accessed
|
||||
// via the API.
|
||||
Key string
|
||||
|
||||
// CreateIndex holds the index corresponding the creation of this KVPair. This
|
||||
// is a read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64
|
||||
|
||||
// LockIndex holds the index corresponding to a lock on this key, if any. This
|
||||
// is a read-only field.
|
||||
LockIndex uint64
|
||||
|
||||
// Flags are any user-defined flags on the key. It is up to the implementer
|
||||
// to check these values, since Consul does not treat them specially.
|
||||
Flags uint64
|
||||
|
||||
// Value is the value for the key. This can be any value, but it will be
|
||||
// base64 encoded upon transport.
|
||||
Value []byte
|
||||
|
||||
// Session is a string representing the ID of the session. Any other
|
||||
// interactions with this key over the same session must specify the same
|
||||
// session ID.
|
||||
Session string
|
||||
|
||||
// Namespace is the namespace the KVPair is associated with
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// KVPairs is a list of KVPair objects
|
||||
type KVPairs []*KVPair
|
||||
|
||||
// KV is used to manipulate the K/V API
|
||||
type KV struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// KV is used to return a handle to the K/V apis
|
||||
func (c *Client) KV() *KV {
|
||||
return &KV{c}
|
||||
}
|
||||
|
||||
// Get is used to lookup a single key. The returned pointer
|
||||
// to the KVPair will be nil if the key does not exist.
|
||||
func (k *KV) Get(key string, q *QueryOptions) (*KVPair, *QueryMeta, error) {
|
||||
resp, qm, err := k.getInternal(key, nil, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, qm, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var entries []*KVPair
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return entries[0], qm, nil
|
||||
}
|
||||
return nil, qm, nil
|
||||
}
|
||||
|
||||
// List is used to lookup all keys under a prefix
|
||||
func (k *KV) List(prefix string, q *QueryOptions) (KVPairs, *QueryMeta, error) {
|
||||
resp, qm, err := k.getInternal(prefix, map[string]string{"recurse": ""}, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, qm, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var entries []*KVPair
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
// Keys is used to list all the keys under a prefix. Optionally,
|
||||
// a separator can be used to limit the responses.
|
||||
func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMeta, error) {
|
||||
params := map[string]string{"keys": ""}
|
||||
if separator != "" {
|
||||
params["separator"] = separator
|
||||
}
|
||||
resp, qm, err := k.getInternal(prefix, params, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, qm, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var entries []string
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) {
|
||||
r := k.c.newRequest("GET", "/v1/kv/"+strings.TrimPrefix(key, "/"))
|
||||
r.setQueryOptions(q)
|
||||
for param, val := range params {
|
||||
r.params.Set(param, val)
|
||||
}
|
||||
rtt, resp, err := k.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
resp.Body.Close()
|
||||
return nil, qm, nil
|
||||
} else if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
return nil, nil, fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
|
||||
}
|
||||
return resp, qm, nil
|
||||
}
|
||||
|
||||
// Put is used to write a new value. Only the
|
||||
// Key, Flags and Value is respected.
|
||||
func (k *KV) Put(p *KVPair, q *WriteOptions) (*WriteMeta, error) {
|
||||
params := make(map[string]string, 1)
|
||||
if p.Flags != 0 {
|
||||
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
||||
}
|
||||
_, wm, err := k.put(p.Key, params, p.Value, q)
|
||||
return wm, err
|
||||
}
|
||||
|
||||
// CAS is used for a Check-And-Set operation. The Key,
|
||||
// ModifyIndex, Flags and Value are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) CAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
params := make(map[string]string, 2)
|
||||
if p.Flags != 0 {
|
||||
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
||||
}
|
||||
params["cas"] = strconv.FormatUint(p.ModifyIndex, 10)
|
||||
return k.put(p.Key, params, p.Value, q)
|
||||
}
|
||||
|
||||
// Acquire is used for a lock acquisition operation. The Key,
|
||||
// Flags, Value and Session are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) Acquire(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
params := make(map[string]string, 2)
|
||||
if p.Flags != 0 {
|
||||
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
||||
}
|
||||
params["acquire"] = p.Session
|
||||
return k.put(p.Key, params, p.Value, q)
|
||||
}
|
||||
|
||||
// Release is used for a lock release operation. The Key,
|
||||
// Flags, Value and Session are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) Release(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
params := make(map[string]string, 2)
|
||||
if p.Flags != 0 {
|
||||
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
||||
}
|
||||
params["release"] = p.Session
|
||||
return k.put(p.Key, params, p.Value, q)
|
||||
}
|
||||
|
||||
func (k *KV) put(key string, params map[string]string, body []byte, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
if len(key) > 0 && key[0] == '/' {
|
||||
return false, nil, fmt.Errorf("Invalid key. Key must not begin with a '/': %s", key)
|
||||
}
|
||||
|
||||
r := k.c.newRequest("PUT", "/v1/kv/"+key)
|
||||
r.setWriteOptions(q)
|
||||
for param, val := range params {
|
||||
r.params.Set(param, val)
|
||||
}
|
||||
r.body = bytes.NewReader(body)
|
||||
rtt, resp, err := requireOK(k.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &WriteMeta{}
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
return res, qm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete a single key
|
||||
func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) {
|
||||
_, qm, err := k.deleteInternal(key, nil, w)
|
||||
return qm, err
|
||||
}
|
||||
|
||||
// DeleteCAS is used for a Delete Check-And-Set operation. The Key
|
||||
// and ModifyIndex are respected. Returns true on success or false on failures.
|
||||
func (k *KV) DeleteCAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
params := map[string]string{
|
||||
"cas": strconv.FormatUint(p.ModifyIndex, 10),
|
||||
}
|
||||
return k.deleteInternal(p.Key, params, q)
|
||||
}
|
||||
|
||||
// DeleteTree is used to delete all keys under a prefix
|
||||
func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) {
|
||||
_, qm, err := k.deleteInternal(prefix, map[string]string{"recurse": ""}, w)
|
||||
return qm, err
|
||||
}
|
||||
|
||||
func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOptions) (bool, *WriteMeta, error) {
|
||||
r := k.c.newRequest("DELETE", "/v1/kv/"+strings.TrimPrefix(key, "/"))
|
||||
r.setWriteOptions(q)
|
||||
for param, val := range params {
|
||||
r.params.Set(param, val)
|
||||
}
|
||||
rtt, resp, err := requireOK(k.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &WriteMeta{}
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
return res, qm, nil
|
||||
}
|
||||
|
||||
// The Txn function has been deprecated from the KV object; please see the Txn
|
||||
// object for more information about Transactions.
|
||||
func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMeta, error) {
|
||||
var ops TxnOps
|
||||
for _, op := range txn {
|
||||
ops = append(ops, &TxnOp{KV: op})
|
||||
}
|
||||
|
||||
respOk, txnResp, qm, err := k.c.txn(ops, q)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
// Convert from the internal format.
|
||||
kvResp := KVTxnResponse{
|
||||
Errors: txnResp.Errors,
|
||||
}
|
||||
for _, result := range txnResp.Results {
|
||||
kvResp.Results = append(kvResp.Results, result.KV)
|
||||
}
|
||||
return respOk, &kvResp, qm, nil
|
||||
}
|
|
@ -0,0 +1,406 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultLockSessionName is the Session Name we assign if none is provided
|
||||
DefaultLockSessionName = "Consul API Lock"
|
||||
|
||||
// DefaultLockSessionTTL is the default session TTL if no Session is provided
|
||||
// when creating a new Lock. This is used because we do not have another
|
||||
// other check to depend upon.
|
||||
DefaultLockSessionTTL = "15s"
|
||||
|
||||
// DefaultLockWaitTime is how long we block for at a time to check if lock
|
||||
// acquisition is possible. This affects the minimum time it takes to cancel
|
||||
// a Lock acquisition.
|
||||
DefaultLockWaitTime = 15 * time.Second
|
||||
|
||||
// DefaultLockRetryTime is how long we wait after a failed lock acquisition
|
||||
// before attempting to do the lock again. This is so that once a lock-delay
|
||||
// is in effect, we do not hot loop retrying the acquisition.
|
||||
DefaultLockRetryTime = 5 * time.Second
|
||||
|
||||
// DefaultMonitorRetryTime is how long we wait after a failed monitor check
|
||||
// of a lock (500 response code). This allows the monitor to ride out brief
|
||||
// periods of unavailability, subject to the MonitorRetries setting in the
|
||||
// lock options which is by default set to 0, disabling this feature. This
|
||||
// affects locks and semaphores.
|
||||
DefaultMonitorRetryTime = 2 * time.Second
|
||||
|
||||
// LockFlagValue is a magic flag we set to indicate a key
|
||||
// is being used for a lock. It is used to detect a potential
|
||||
// conflict with a semaphore.
|
||||
LockFlagValue = 0x2ddccbc058a50c18
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrLockHeld is returned if we attempt to double lock
|
||||
ErrLockHeld = fmt.Errorf("Lock already held")
|
||||
|
||||
// ErrLockNotHeld is returned if we attempt to unlock a lock
|
||||
// that we do not hold.
|
||||
ErrLockNotHeld = fmt.Errorf("Lock not held")
|
||||
|
||||
// ErrLockInUse is returned if we attempt to destroy a lock
|
||||
// that is in use.
|
||||
ErrLockInUse = fmt.Errorf("Lock in use")
|
||||
|
||||
// ErrLockConflict is returned if the flags on a key
|
||||
// used for a lock do not match expectation
|
||||
ErrLockConflict = fmt.Errorf("Existing key does not match lock use")
|
||||
)
|
||||
|
||||
// Lock is used to implement client-side leader election. It is follows the
|
||||
// algorithm as described here: https://www.consul.io/docs/guides/leader-election.html.
|
||||
type Lock struct {
|
||||
c *Client
|
||||
opts *LockOptions
|
||||
|
||||
isHeld bool
|
||||
sessionRenew chan struct{}
|
||||
lockSession string
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
// LockOptions is used to parameterize the Lock behavior.
|
||||
type LockOptions struct {
|
||||
Key string // Must be set and have write permissions
|
||||
Value []byte // Optional, value to associate with the lock
|
||||
Session string // Optional, created if not specified
|
||||
SessionOpts *SessionEntry // Optional, options to use when creating a session
|
||||
SessionName string // Optional, defaults to DefaultLockSessionName (ignored if SessionOpts is given)
|
||||
SessionTTL string // Optional, defaults to DefaultLockSessionTTL (ignored if SessionOpts is given)
|
||||
MonitorRetries int // Optional, defaults to 0 which means no retries
|
||||
MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime
|
||||
LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime
|
||||
LockTryOnce bool // Optional, defaults to false which means try forever
|
||||
Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace
|
||||
}
|
||||
|
||||
// LockKey returns a handle to a lock struct which can be used
|
||||
// to acquire and release the mutex. The key used must have
|
||||
// write permissions.
|
||||
func (c *Client) LockKey(key string) (*Lock, error) {
|
||||
opts := &LockOptions{
|
||||
Key: key,
|
||||
}
|
||||
return c.LockOpts(opts)
|
||||
}
|
||||
|
||||
// LockOpts returns a handle to a lock struct which can be used
|
||||
// to acquire and release the mutex. The key used must have
|
||||
// write permissions.
|
||||
func (c *Client) LockOpts(opts *LockOptions) (*Lock, error) {
|
||||
if opts.Key == "" {
|
||||
return nil, fmt.Errorf("missing key")
|
||||
}
|
||||
if opts.SessionName == "" {
|
||||
opts.SessionName = DefaultLockSessionName
|
||||
}
|
||||
if opts.SessionTTL == "" {
|
||||
opts.SessionTTL = DefaultLockSessionTTL
|
||||
} else {
|
||||
if _, err := time.ParseDuration(opts.SessionTTL); err != nil {
|
||||
return nil, fmt.Errorf("invalid SessionTTL: %v", err)
|
||||
}
|
||||
}
|
||||
if opts.MonitorRetryTime == 0 {
|
||||
opts.MonitorRetryTime = DefaultMonitorRetryTime
|
||||
}
|
||||
if opts.LockWaitTime == 0 {
|
||||
opts.LockWaitTime = DefaultLockWaitTime
|
||||
}
|
||||
l := &Lock{
|
||||
c: c,
|
||||
opts: opts,
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Lock attempts to acquire the lock and blocks while doing so.
|
||||
// Providing a non-nil stopCh can be used to abort the lock attempt.
|
||||
// Returns a channel that is closed if our lock is lost or an error.
|
||||
// This channel could be closed at any time due to session invalidation,
|
||||
// communication errors, operator intervention, etc. It is NOT safe to
|
||||
// assume that the lock is held until Unlock() unless the Session is specifically
|
||||
// created without any associated health checks. By default Consul sessions
|
||||
// prefer liveness over safety and an application must be able to handle
|
||||
// the lock being lost.
|
||||
func (l *Lock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) {
|
||||
// Hold the lock as we try to acquire
|
||||
l.l.Lock()
|
||||
defer l.l.Unlock()
|
||||
|
||||
// Check if we already hold the lock
|
||||
if l.isHeld {
|
||||
return nil, ErrLockHeld
|
||||
}
|
||||
|
||||
wOpts := WriteOptions{
|
||||
Namespace: l.opts.Namespace,
|
||||
}
|
||||
|
||||
// Check if we need to create a session first
|
||||
l.lockSession = l.opts.Session
|
||||
if l.lockSession == "" {
|
||||
s, err := l.createSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session: %v", err)
|
||||
}
|
||||
|
||||
l.sessionRenew = make(chan struct{})
|
||||
l.lockSession = s
|
||||
|
||||
session := l.c.Session()
|
||||
go session.RenewPeriodic(l.opts.SessionTTL, s, &wOpts, l.sessionRenew)
|
||||
|
||||
// If we fail to acquire the lock, cleanup the session
|
||||
defer func() {
|
||||
if !l.isHeld {
|
||||
close(l.sessionRenew)
|
||||
l.sessionRenew = nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Setup the query options
|
||||
kv := l.c.KV()
|
||||
qOpts := QueryOptions{
|
||||
WaitTime: l.opts.LockWaitTime,
|
||||
Namespace: l.opts.Namespace,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
attempts := 0
|
||||
WAIT:
|
||||
// Check if we should quit
|
||||
select {
|
||||
case <-stopCh:
|
||||
return nil, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Handle the one-shot mode.
|
||||
if l.opts.LockTryOnce && attempts > 0 {
|
||||
elapsed := time.Since(start)
|
||||
if elapsed > l.opts.LockWaitTime {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Query wait time should not exceed the lock wait time
|
||||
qOpts.WaitTime = l.opts.LockWaitTime - elapsed
|
||||
}
|
||||
attempts++
|
||||
|
||||
// Look for an existing lock, blocking until not taken
|
||||
pair, meta, err := kv.Get(l.opts.Key, &qOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read lock: %v", err)
|
||||
}
|
||||
if pair != nil && pair.Flags != LockFlagValue {
|
||||
return nil, ErrLockConflict
|
||||
}
|
||||
locked := false
|
||||
if pair != nil && pair.Session == l.lockSession {
|
||||
goto HELD
|
||||
}
|
||||
if pair != nil && pair.Session != "" {
|
||||
qOpts.WaitIndex = meta.LastIndex
|
||||
goto WAIT
|
||||
}
|
||||
|
||||
// Try to acquire the lock
|
||||
pair = l.lockEntry(l.lockSession)
|
||||
|
||||
locked, _, err = kv.Acquire(pair, &wOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to acquire lock: %v", err)
|
||||
}
|
||||
|
||||
// Handle the case of not getting the lock
|
||||
if !locked {
|
||||
// Determine why the lock failed
|
||||
qOpts.WaitIndex = 0
|
||||
pair, meta, err = kv.Get(l.opts.Key, &qOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pair != nil && pair.Session != "" {
|
||||
//If the session is not null, this means that a wait can safely happen
|
||||
//using a long poll
|
||||
qOpts.WaitIndex = meta.LastIndex
|
||||
goto WAIT
|
||||
} else {
|
||||
// If the session is empty and the lock failed to acquire, then it means
|
||||
// a lock-delay is in effect and a timed wait must be used
|
||||
select {
|
||||
case <-time.After(DefaultLockRetryTime):
|
||||
goto WAIT
|
||||
case <-stopCh:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HELD:
|
||||
// Watch to ensure we maintain leadership
|
||||
leaderCh := make(chan struct{})
|
||||
go l.monitorLock(l.lockSession, leaderCh)
|
||||
|
||||
// Set that we own the lock
|
||||
l.isHeld = true
|
||||
|
||||
// Locked! All done
|
||||
return leaderCh, nil
|
||||
}
|
||||
|
||||
// Unlock released the lock. It is an error to call this
|
||||
// if the lock is not currently held.
|
||||
func (l *Lock) Unlock() error {
|
||||
// Hold the lock as we try to release
|
||||
l.l.Lock()
|
||||
defer l.l.Unlock()
|
||||
|
||||
// Ensure the lock is actually held
|
||||
if !l.isHeld {
|
||||
return ErrLockNotHeld
|
||||
}
|
||||
|
||||
// Set that we no longer own the lock
|
||||
l.isHeld = false
|
||||
|
||||
// Stop the session renew
|
||||
if l.sessionRenew != nil {
|
||||
defer func() {
|
||||
close(l.sessionRenew)
|
||||
l.sessionRenew = nil
|
||||
}()
|
||||
}
|
||||
|
||||
// Get the lock entry, and clear the lock session
|
||||
lockEnt := l.lockEntry(l.lockSession)
|
||||
l.lockSession = ""
|
||||
|
||||
// Release the lock explicitly
|
||||
kv := l.c.KV()
|
||||
w := WriteOptions{Namespace: l.opts.Namespace}
|
||||
|
||||
_, _, err := kv.Release(lockEnt, &w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to release lock: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy is used to cleanup the lock entry. It is not necessary
|
||||
// to invoke. It will fail if the lock is in use.
|
||||
func (l *Lock) Destroy() error {
|
||||
// Hold the lock as we try to release
|
||||
l.l.Lock()
|
||||
defer l.l.Unlock()
|
||||
|
||||
// Check if we already hold the lock
|
||||
if l.isHeld {
|
||||
return ErrLockHeld
|
||||
}
|
||||
|
||||
// Look for an existing lock
|
||||
kv := l.c.KV()
|
||||
q := QueryOptions{Namespace: l.opts.Namespace}
|
||||
|
||||
pair, _, err := kv.Get(l.opts.Key, &q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read lock: %v", err)
|
||||
}
|
||||
|
||||
// Nothing to do if the lock does not exist
|
||||
if pair == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for possible flag conflict
|
||||
if pair.Flags != LockFlagValue {
|
||||
return ErrLockConflict
|
||||
}
|
||||
|
||||
// Check if it is in use
|
||||
if pair.Session != "" {
|
||||
return ErrLockInUse
|
||||
}
|
||||
|
||||
// Attempt the delete
|
||||
w := WriteOptions{Namespace: l.opts.Namespace}
|
||||
didRemove, _, err := kv.DeleteCAS(pair, &w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove lock: %v", err)
|
||||
}
|
||||
if !didRemove {
|
||||
return ErrLockInUse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSession is used to create a new managed session
|
||||
func (l *Lock) createSession() (string, error) {
|
||||
session := l.c.Session()
|
||||
se := l.opts.SessionOpts
|
||||
if se == nil {
|
||||
se = &SessionEntry{
|
||||
Name: l.opts.SessionName,
|
||||
TTL: l.opts.SessionTTL,
|
||||
}
|
||||
}
|
||||
w := WriteOptions{Namespace: l.opts.Namespace}
|
||||
id, _, err := session.Create(se, &w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// lockEntry returns a formatted KVPair for the lock
|
||||
func (l *Lock) lockEntry(session string) *KVPair {
|
||||
return &KVPair{
|
||||
Key: l.opts.Key,
|
||||
Value: l.opts.Value,
|
||||
Session: session,
|
||||
Flags: LockFlagValue,
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLock is a long running routine to monitor a lock ownership
|
||||
// It closes the stopCh if we lose our leadership.
|
||||
func (l *Lock) monitorLock(session string, stopCh chan struct{}) {
|
||||
defer close(stopCh)
|
||||
kv := l.c.KV()
|
||||
opts := QueryOptions{
|
||||
RequireConsistent: true,
|
||||
Namespace: l.opts.Namespace,
|
||||
}
|
||||
WAIT:
|
||||
retries := l.opts.MonitorRetries
|
||||
RETRY:
|
||||
pair, meta, err := kv.Get(l.opts.Key, &opts)
|
||||
if err != nil {
|
||||
// If configured we can try to ride out a brief Consul unavailability
|
||||
// by doing retries. Note that we have to attempt the retry in a non-
|
||||
// blocking fashion so that we have a clean place to reset the retry
|
||||
// counter if service is restored.
|
||||
if retries > 0 && IsRetryableError(err) {
|
||||
time.Sleep(l.opts.MonitorRetryTime)
|
||||
retries--
|
||||
opts.WaitIndex = 0
|
||||
goto RETRY
|
||||
}
|
||||
return
|
||||
}
|
||||
if pair != nil && pair.Session == session {
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
goto WAIT
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Namespace is the configuration of a single namespace. Namespacing is a Consul Enterprise feature.
|
||||
type Namespace struct {
|
||||
// Name is the name of the Namespace. It must be unique and
|
||||
// must be a DNS hostname. There are also other reserved names
|
||||
// that may not be used.
|
||||
Name string `json:"Name"`
|
||||
|
||||
// Description is where the user puts any information they want
|
||||
// about the namespace. It is not used internally.
|
||||
Description string `json:"Description,omitempty"`
|
||||
|
||||
// ACLs is the configuration of ACLs for this namespace. It has its
|
||||
// own struct so that we can add more to it in the future.
|
||||
// This is nullable so that we can omit if empty when encoding in JSON
|
||||
ACLs *NamespaceACLConfig `json:"ACLs,omitempty"`
|
||||
|
||||
// Meta is a map that can be used to add kv metadata to the namespace definition
|
||||
Meta map[string]string `json:"Meta,omitempty"`
|
||||
|
||||
// DeletedAt is the time when the Namespace was marked for deletion
|
||||
// This is nullable so that we can omit if empty when encoding in JSON
|
||||
DeletedAt *time.Time `json:"DeletedAt,omitempty"`
|
||||
|
||||
// CreateIndex is the Raft index at which the Namespace was created
|
||||
CreateIndex uint64 `json:"CreateIndex,omitempty"`
|
||||
|
||||
// ModifyIndex is the latest Raft index at which the Namespace was modified.
|
||||
ModifyIndex uint64 `json:"ModifyIndex,omitempty"`
|
||||
}
|
||||
|
||||
// NamespaceACLConfig is the Namespace specific ACL configuration container
|
||||
type NamespaceACLConfig struct {
|
||||
// PolicyDefaults is the list of policies that should be used for the parent authorizer
|
||||
// of all tokens in the associated namespace.
|
||||
PolicyDefaults []ACLLink `json:"PolicyDefaults"`
|
||||
// RoleDefaults is the list of roles that should be used for the parent authorizer
|
||||
// of all tokens in the associated namespace.
|
||||
RoleDefaults []ACLLink `json:"RoleDefaults"`
|
||||
}
|
||||
|
||||
// Namespaces can be used to manage Namespaces in Consul Enterprise..
|
||||
type Namespaces struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Operator returns a handle to the operator endpoints.
|
||||
func (c *Client) Namespaces() *Namespaces {
|
||||
return &Namespaces{c}
|
||||
}
|
||||
|
||||
func (n *Namespaces) Create(ns *Namespace, q *WriteOptions) (*Namespace, *WriteMeta, error) {
|
||||
if ns.Name == "" {
|
||||
return nil, nil, fmt.Errorf("Must specify a Name for Namespace creation")
|
||||
}
|
||||
|
||||
r := n.c.newRequest("PUT", "/v1/namespace")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = ns
|
||||
rtt, resp, err := requireOK(n.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
var out Namespace
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &out, wm, nil
|
||||
}
|
||||
|
||||
func (n *Namespaces) Update(ns *Namespace, q *WriteOptions) (*Namespace, *WriteMeta, error) {
|
||||
if ns.Name == "" {
|
||||
return nil, nil, fmt.Errorf("Must specify a Name for Namespace updating")
|
||||
}
|
||||
|
||||
r := n.c.newRequest("PUT", "/v1/namespace/"+ns.Name)
|
||||
r.setWriteOptions(q)
|
||||
r.obj = ns
|
||||
rtt, resp, err := requireOK(n.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
var out Namespace
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &out, wm, nil
|
||||
}
|
||||
|
||||
func (n *Namespaces) Read(name string, q *QueryOptions) (*Namespace, *QueryMeta, error) {
|
||||
var out Namespace
|
||||
r := n.c.newRequest("GET", "/v1/namespace/"+name)
|
||||
r.setQueryOptions(q)
|
||||
found, rtt, resp, err := requireNotFoundOrOK(n.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if !found {
|
||||
return nil, qm, nil
|
||||
}
|
||||
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &out, qm, nil
|
||||
}
|
||||
|
||||
func (n *Namespaces) Delete(name string, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := n.c.newRequest("DELETE", "/v1/namespace/"+name)
|
||||
r.setWriteOptions(q)
|
||||
rtt, resp, err := requireOK(n.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
func (n *Namespaces) List(q *QueryOptions) ([]*Namespace, *QueryMeta, error) {
|
||||
var out []*Namespace
|
||||
r := n.c.newRequest("GET", "/v1/namespaces")
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(n.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package api
|
||||
|
||||
// Operator can be used to perform low-level operator tasks for Consul.
|
||||
type Operator struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Operator returns a handle to the operator endpoints.
|
||||
func (c *Client) Operator() *Operator {
|
||||
return &Operator{c}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package api
|
||||
|
||||
// The /v1/operator/area endpoints are available only in Consul Enterprise and
|
||||
// interact with its network area subsystem. Network areas are used to link
|
||||
// together Consul servers in different Consul datacenters. With network areas,
|
||||
// Consul datacenters can be linked together in ways other than a fully-connected
|
||||
// mesh, as is required for Consul's WAN.
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Area defines a network area.
|
||||
type Area struct {
|
||||
// ID is this identifier for an area (a UUID). This must be left empty
|
||||
// when creating a new area.
|
||||
ID string
|
||||
|
||||
// PeerDatacenter is the peer Consul datacenter that will make up the
|
||||
// other side of this network area. Network areas always involve a pair
|
||||
// of datacenters: the datacenter where the area was created, and the
|
||||
// peer datacenter. This is required.
|
||||
PeerDatacenter string
|
||||
|
||||
// RetryJoin specifies the address of Consul servers to join to, such as
|
||||
// an IPs or hostnames with an optional port number. This is optional.
|
||||
RetryJoin []string
|
||||
|
||||
// UseTLS specifies whether gossip over this area should be encrypted with TLS
|
||||
// if possible.
|
||||
UseTLS bool
|
||||
}
|
||||
|
||||
// AreaJoinResponse is returned when a join occurs and gives the result for each
|
||||
// address.
|
||||
type AreaJoinResponse struct {
|
||||
// The address that was joined.
|
||||
Address string
|
||||
|
||||
// Whether or not the join was a success.
|
||||
Joined bool
|
||||
|
||||
// If we couldn't join, this is the message with information.
|
||||
Error string
|
||||
}
|
||||
|
||||
// SerfMember is a generic structure for reporting information about members in
|
||||
// a Serf cluster. This is only used by the area endpoints right now, but this
|
||||
// could be expanded to other endpoints in the future.
|
||||
type SerfMember struct {
|
||||
// ID is the node identifier (a UUID).
|
||||
ID string
|
||||
|
||||
// Name is the node name.
|
||||
Name string
|
||||
|
||||
// Addr has the IP address.
|
||||
Addr net.IP
|
||||
|
||||
// Port is the RPC port.
|
||||
Port uint16
|
||||
|
||||
// Datacenter is the DC name.
|
||||
Datacenter string
|
||||
|
||||
// Role is "client", "server", or "unknown".
|
||||
Role string
|
||||
|
||||
// Build has the version of the Consul agent.
|
||||
Build string
|
||||
|
||||
// Protocol is the protocol of the Consul agent.
|
||||
Protocol int
|
||||
|
||||
// Status is the Serf health status "none", "alive", "leaving", "left",
|
||||
// or "failed".
|
||||
Status string
|
||||
|
||||
// RTT is the estimated round trip time from the server handling the
|
||||
// request to the this member. This will be negative if no RTT estimate
|
||||
// is available.
|
||||
RTT time.Duration
|
||||
}
|
||||
|
||||
// AreaCreate will create a new network area. The ID in the given structure must
|
||||
// be empty and a generated ID will be returned on success.
|
||||
func (op *Operator) AreaCreate(area *Area, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
r := op.c.newRequest("POST", "/v1/operator/area")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = area
|
||||
rtt, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
var out struct{ ID string }
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// AreaUpdate will update the configuration of the network area with the given ID.
|
||||
func (op *Operator) AreaUpdate(areaID string, area *Area, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
r := op.c.newRequest("PUT", "/v1/operator/area/"+areaID)
|
||||
r.setWriteOptions(q)
|
||||
r.obj = area
|
||||
rtt, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
var out struct{ ID string }
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// AreaGet returns a single network area.
|
||||
func (op *Operator) AreaGet(areaID string, q *QueryOptions) ([]*Area, *QueryMeta, error) {
|
||||
var out []*Area
|
||||
qm, err := op.c.query("/v1/operator/area/"+areaID, &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// AreaList returns all the available network areas.
|
||||
func (op *Operator) AreaList(q *QueryOptions) ([]*Area, *QueryMeta, error) {
|
||||
var out []*Area
|
||||
qm, err := op.c.query("/v1/operator/area", &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// AreaDelete deletes the given network area.
|
||||
func (op *Operator) AreaDelete(areaID string, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := op.c.newRequest("DELETE", "/v1/operator/area/"+areaID)
|
||||
r.setWriteOptions(q)
|
||||
rtt, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// AreaJoin attempts to join the given set of join addresses to the given
|
||||
// network area. See the Area structure for details about join addresses.
|
||||
func (op *Operator) AreaJoin(areaID string, addresses []string, q *WriteOptions) ([]*AreaJoinResponse, *WriteMeta, error) {
|
||||
r := op.c.newRequest("PUT", "/v1/operator/area/"+areaID+"/join")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = addresses
|
||||
rtt, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
var out []*AreaJoinResponse
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, wm, nil
|
||||
}
|
||||
|
||||
// AreaMembers lists the Serf information about the members in the given area.
|
||||
func (op *Operator) AreaMembers(areaID string, q *QueryOptions) ([]*SerfMember, *QueryMeta, error) {
|
||||
var out []*SerfMember
|
||||
qm, err := op.c.query("/v1/operator/area/"+areaID+"/members", &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AutopilotConfiguration is used for querying/setting the Autopilot configuration.
|
||||
// Autopilot helps manage operator tasks related to Consul servers like removing
|
||||
// failed servers from the Raft quorum.
|
||||
type AutopilotConfiguration struct {
|
||||
// CleanupDeadServers controls whether to remove dead servers from the Raft
|
||||
// peer list when a new server joins
|
||||
CleanupDeadServers bool
|
||||
|
||||
// LastContactThreshold is the limit on the amount of time a server can go
|
||||
// without leader contact before being considered unhealthy.
|
||||
LastContactThreshold *ReadableDuration
|
||||
|
||||
// MaxTrailingLogs is the amount of entries in the Raft Log that a server can
|
||||
// be behind before being considered unhealthy.
|
||||
MaxTrailingLogs uint64
|
||||
|
||||
// MinQuorum sets the minimum number of servers allowed in a cluster before
|
||||
// autopilot can prune dead servers.
|
||||
MinQuorum uint
|
||||
|
||||
// ServerStabilizationTime is the minimum amount of time a server must be
|
||||
// in a stable, healthy state before it can be added to the cluster. Only
|
||||
// applicable with Raft protocol version 3 or higher.
|
||||
ServerStabilizationTime *ReadableDuration
|
||||
|
||||
// (Enterprise-only) RedundancyZoneTag is the node tag to use for separating
|
||||
// servers into zones for redundancy. If left blank, this feature will be disabled.
|
||||
RedundancyZoneTag string
|
||||
|
||||
// (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration
|
||||
// strategy of waiting until enough newer-versioned servers have been added to the
|
||||
// cluster before promoting them to voters.
|
||||
DisableUpgradeMigration bool
|
||||
|
||||
// (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when
|
||||
// performing upgrade migrations. If left blank, the Consul version will be used.
|
||||
UpgradeVersionTag string
|
||||
|
||||
// CreateIndex holds the index corresponding the creation of this configuration.
|
||||
// This is a read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex will be set to the index of the last update when retrieving the
|
||||
// Autopilot configuration. Resubmitting a configuration with
|
||||
// AutopilotCASConfiguration will perform a check-and-set operation which ensures
|
||||
// there hasn't been a subsequent update since the configuration was retrieved.
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// ServerHealth is the health (from the leader's point of view) of a server.
|
||||
type ServerHealth struct {
|
||||
// ID is the raft ID of the server.
|
||||
ID string
|
||||
|
||||
// Name is the node name of the server.
|
||||
Name string
|
||||
|
||||
// Address is the address of the server.
|
||||
Address string
|
||||
|
||||
// The status of the SerfHealth check for the server.
|
||||
SerfStatus string
|
||||
|
||||
// Version is the Consul version of the server.
|
||||
Version string
|
||||
|
||||
// Leader is whether this server is currently the leader.
|
||||
Leader bool
|
||||
|
||||
// LastContact is the time since this node's last contact with the leader.
|
||||
LastContact *ReadableDuration
|
||||
|
||||
// LastTerm is the highest leader term this server has a record of in its Raft log.
|
||||
LastTerm uint64
|
||||
|
||||
// LastIndex is the last log index this server has a record of in its Raft log.
|
||||
LastIndex uint64
|
||||
|
||||
// Healthy is whether or not the server is healthy according to the current
|
||||
// Autopilot config.
|
||||
Healthy bool
|
||||
|
||||
// Voter is whether this is a voting server.
|
||||
Voter bool
|
||||
|
||||
// StableSince is the last time this server's Healthy value changed.
|
||||
StableSince time.Time
|
||||
}
|
||||
|
||||
// OperatorHealthReply is a representation of the overall health of the cluster
|
||||
type OperatorHealthReply struct {
|
||||
// Healthy is true if all the servers in the cluster are healthy.
|
||||
Healthy bool
|
||||
|
||||
// FailureTolerance is the number of healthy servers that could be lost without
|
||||
// an outage occurring.
|
||||
FailureTolerance int
|
||||
|
||||
// Servers holds the health of each server.
|
||||
Servers []ServerHealth
|
||||
}
|
||||
|
||||
// ReadableDuration is a duration type that is serialized to JSON in human readable format.
|
||||
type ReadableDuration time.Duration
|
||||
|
||||
func NewReadableDuration(dur time.Duration) *ReadableDuration {
|
||||
d := ReadableDuration(dur)
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *ReadableDuration) String() string {
|
||||
return d.Duration().String()
|
||||
}
|
||||
|
||||
func (d *ReadableDuration) Duration() time.Duration {
|
||||
if d == nil {
|
||||
return time.Duration(0)
|
||||
}
|
||||
return time.Duration(*d)
|
||||
}
|
||||
|
||||
func (d *ReadableDuration) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(`"%s"`, d.Duration().String())), nil
|
||||
}
|
||||
|
||||
func (d *ReadableDuration) UnmarshalJSON(raw []byte) (err error) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("cannot unmarshal to nil pointer")
|
||||
}
|
||||
|
||||
var dur time.Duration
|
||||
str := string(raw)
|
||||
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
|
||||
// quoted string
|
||||
dur, err = time.ParseDuration(str[1 : len(str)-1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// no quotes, not a string
|
||||
v, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dur = time.Duration(v)
|
||||
}
|
||||
|
||||
*d = ReadableDuration(dur)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutopilotGetConfiguration is used to query the current Autopilot configuration.
|
||||
func (op *Operator) AutopilotGetConfiguration(q *QueryOptions) (*AutopilotConfiguration, error) {
|
||||
r := op.c.newRequest("GET", "/v1/operator/autopilot/configuration")
|
||||
r.setQueryOptions(q)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out AutopilotConfiguration
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AutopilotSetConfiguration is used to set the current Autopilot configuration.
|
||||
func (op *Operator) AutopilotSetConfiguration(conf *AutopilotConfiguration, q *WriteOptions) error {
|
||||
r := op.c.newRequest("PUT", "/v1/operator/autopilot/configuration")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = conf
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutopilotCASConfiguration is used to perform a Check-And-Set update on the
|
||||
// Autopilot configuration. The ModifyIndex value will be respected. Returns
|
||||
// true on success or false on failures.
|
||||
func (op *Operator) AutopilotCASConfiguration(conf *AutopilotConfiguration, q *WriteOptions) (bool, error) {
|
||||
r := op.c.newRequest("PUT", "/v1/operator/autopilot/configuration")
|
||||
r.setWriteOptions(q)
|
||||
r.params.Set("cas", strconv.FormatUint(conf.ModifyIndex, 10))
|
||||
r.obj = conf
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// AutopilotServerHealth
|
||||
func (op *Operator) AutopilotServerHealth(q *QueryOptions) (*OperatorHealthReply, error) {
|
||||
r := op.c.newRequest("GET", "/v1/operator/autopilot/health")
|
||||
r.setQueryOptions(q)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out OperatorHealthReply
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package api
|
||||
|
||||
// keyringRequest is used for performing Keyring operations
|
||||
type keyringRequest struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
// KeyringResponse is returned when listing the gossip encryption keys
|
||||
type KeyringResponse struct {
|
||||
// Whether this response is for a WAN ring
|
||||
WAN bool
|
||||
|
||||
// The datacenter name this request corresponds to
|
||||
Datacenter string
|
||||
|
||||
// Segment has the network segment this request corresponds to.
|
||||
Segment string
|
||||
|
||||
// Messages has information or errors from serf
|
||||
Messages map[string]string `json:",omitempty"`
|
||||
|
||||
// A map of the encryption keys to the number of nodes they're installed on
|
||||
Keys map[string]int
|
||||
|
||||
// A map of the encryption primary keys to the number of nodes they're installed on
|
||||
PrimaryKeys map[string]int
|
||||
|
||||
// The total number of nodes in this ring
|
||||
NumNodes int
|
||||
}
|
||||
|
||||
// KeyringInstall is used to install a new gossip encryption key into the cluster
|
||||
func (op *Operator) KeyringInstall(key string, q *WriteOptions) error {
|
||||
r := op.c.newRequest("POST", "/v1/operator/keyring")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = keyringRequest{
|
||||
Key: key,
|
||||
}
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyringList is used to list the gossip keys installed in the cluster
|
||||
func (op *Operator) KeyringList(q *QueryOptions) ([]*KeyringResponse, error) {
|
||||
r := op.c.newRequest("GET", "/v1/operator/keyring")
|
||||
r.setQueryOptions(q)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out []*KeyringResponse
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KeyringRemove is used to remove a gossip encryption key from the cluster
|
||||
func (op *Operator) KeyringRemove(key string, q *WriteOptions) error {
|
||||
r := op.c.newRequest("DELETE", "/v1/operator/keyring")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = keyringRequest{
|
||||
Key: key,
|
||||
}
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyringUse is used to change the active gossip encryption key
|
||||
func (op *Operator) KeyringUse(key string, q *WriteOptions) error {
|
||||
r := op.c.newRequest("PUT", "/v1/operator/keyring")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = keyringRequest{
|
||||
Key: key,
|
||||
}
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type License struct {
|
||||
// The unique identifier of the license
|
||||
LicenseID string `json:"license_id"`
|
||||
|
||||
// The customer ID associated with the license
|
||||
CustomerID string `json:"customer_id"`
|
||||
|
||||
// If set, an identifier that should be used to lock the license to a
|
||||
// particular site, cluster, etc.
|
||||
InstallationID string `json:"installation_id"`
|
||||
|
||||
// The time at which the license was issued
|
||||
IssueTime time.Time `json:"issue_time"`
|
||||
|
||||
// The time at which the license starts being valid
|
||||
StartTime time.Time `json:"start_time"`
|
||||
|
||||
// The time after which the license expires
|
||||
ExpirationTime time.Time `json:"expiration_time"`
|
||||
|
||||
// The time at which the license ceases to function and can
|
||||
// no longer be used in any capacity
|
||||
TerminationTime time.Time `json:"termination_time"`
|
||||
|
||||
// The product the license is valid for
|
||||
Product string `json:"product"`
|
||||
|
||||
// License Specific Flags
|
||||
Flags map[string]interface{} `json:"flags"`
|
||||
|
||||
// Modules is a list of the licensed enterprise modules
|
||||
Modules []string `json:"modules"`
|
||||
|
||||
// List of features enabled by the license
|
||||
Features []string `json:"features"`
|
||||
}
|
||||
|
||||
type LicenseReply struct {
|
||||
Valid bool
|
||||
License *License
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
func (op *Operator) LicenseGet(q *QueryOptions) (*LicenseReply, error) {
|
||||
var reply LicenseReply
|
||||
if _, err := op.c.query("/v1/operator/license", &reply, q); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &reply, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (op *Operator) LicenseGetSigned(q *QueryOptions) (string, error) {
|
||||
r := op.c.newRequest("GET", "/v1/operator/license")
|
||||
r.params.Set("signed", "1")
|
||||
r.setQueryOptions(q)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// LicenseReset will reset the license to the builtin one if it is still valid.
|
||||
// If the builtin license is invalid, the current license stays active.
|
||||
func (op *Operator) LicenseReset(opts *WriteOptions) (*LicenseReply, error) {
|
||||
var reply LicenseReply
|
||||
r := op.c.newRequest("DELETE", "/v1/operator/license")
|
||||
r.setWriteOptions(opts)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := decodeBody(resp, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &reply, nil
|
||||
}
|
||||
|
||||
func (op *Operator) LicensePut(license string, opts *WriteOptions) (*LicenseReply, error) {
|
||||
var reply LicenseReply
|
||||
r := op.c.newRequest("PUT", "/v1/operator/license")
|
||||
r.setWriteOptions(opts)
|
||||
r.body = strings.NewReader(license)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := decodeBody(resp, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &reply, nil
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package api
|
||||
|
||||
// RaftServer has information about a server in the Raft configuration.
|
||||
type RaftServer struct {
|
||||
// ID is the unique ID for the server. These are currently the same
|
||||
// as the address, but they will be changed to a real GUID in a future
|
||||
// release of Consul.
|
||||
ID string
|
||||
|
||||
// Node is the node name of the server, as known by Consul, or this
|
||||
// will be set to "(unknown)" otherwise.
|
||||
Node string
|
||||
|
||||
// Address is the IP:port of the server, used for Raft communications.
|
||||
Address string
|
||||
|
||||
// Leader is true if this server is the current cluster leader.
|
||||
Leader bool
|
||||
|
||||
// Protocol version is the raft protocol version used by the server
|
||||
ProtocolVersion string
|
||||
|
||||
// Voter is true if this server has a vote in the cluster. This might
|
||||
// be false if the server is staging and still coming online, or if
|
||||
// it's a non-voting server, which will be added in a future release of
|
||||
// Consul.
|
||||
Voter bool
|
||||
}
|
||||
|
||||
// RaftConfiguration is returned when querying for the current Raft configuration.
|
||||
type RaftConfiguration struct {
|
||||
// Servers has the list of servers in the Raft configuration.
|
||||
Servers []*RaftServer
|
||||
|
||||
// Index has the Raft index of this configuration.
|
||||
Index uint64
|
||||
}
|
||||
|
||||
// RaftGetConfiguration is used to query the current Raft peer set.
|
||||
func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) {
|
||||
r := op.c.newRequest("GET", "/v1/operator/raft/configuration")
|
||||
r.setQueryOptions(q)
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out RaftConfiguration
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// RaftRemovePeerByAddress is used to kick a stale peer (one that it in the Raft
|
||||
// quorum but no longer known to Serf or the catalog) by address in the form of
|
||||
// "IP:port".
|
||||
func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) error {
|
||||
r := op.c.newRequest("DELETE", "/v1/operator/raft/peer")
|
||||
r.setWriteOptions(q)
|
||||
|
||||
r.params.Set("address", address)
|
||||
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RaftRemovePeerByID is used to kick a stale peer (one that it in the Raft
|
||||
// quorum but no longer known to Serf or the catalog) by ID.
|
||||
func (op *Operator) RaftRemovePeerByID(id string, q *WriteOptions) error {
|
||||
r := op.c.newRequest("DELETE", "/v1/operator/raft/peer")
|
||||
r.setWriteOptions(q)
|
||||
|
||||
r.params.Set("id", id)
|
||||
|
||||
_, resp, err := requireOK(op.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package api
|
||||
|
||||
// SegmentList returns all the available LAN segments.
|
||||
func (op *Operator) SegmentList(q *QueryOptions) ([]string, *QueryMeta, error) {
|
||||
var out []string
|
||||
qm, err := op.c.query("/v1/operator/segment", &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
package api
|
||||
|
||||
// QueryDatacenterOptions sets options about how we fail over if there are no
|
||||
// healthy nodes in the local datacenter.
|
||||
type QueryDatacenterOptions struct {
|
||||
// NearestN is set to the number of remote datacenters to try, based on
|
||||
// network coordinates.
|
||||
NearestN int
|
||||
|
||||
// Datacenters is a fixed list of datacenters to try after NearestN. We
|
||||
// never try a datacenter multiple times, so those are subtracted from
|
||||
// this list before proceeding.
|
||||
Datacenters []string
|
||||
}
|
||||
|
||||
// QueryDNSOptions controls settings when query results are served over DNS.
|
||||
type QueryDNSOptions struct {
|
||||
// TTL is the time to live for the served DNS results.
|
||||
TTL string
|
||||
}
|
||||
|
||||
// ServiceQuery is used to query for a set of healthy nodes offering a specific
|
||||
// service.
|
||||
type ServiceQuery struct {
|
||||
// Service is the service to query.
|
||||
Service string
|
||||
|
||||
// Namespace of the service to query
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// Near allows baking in the name of a node to automatically distance-
|
||||
// sort from. The magic "_agent" value is supported, which sorts near
|
||||
// the agent which initiated the request by default.
|
||||
Near string
|
||||
|
||||
// Failover controls what we do if there are no healthy nodes in the
|
||||
// local datacenter.
|
||||
Failover QueryDatacenterOptions
|
||||
|
||||
// IgnoreCheckIDs is an optional list of health check IDs to ignore when
|
||||
// considering which nodes are healthy. It is useful as an emergency measure
|
||||
// to temporarily override some health check that is producing false negatives
|
||||
// for example.
|
||||
IgnoreCheckIDs []string
|
||||
|
||||
// If OnlyPassing is true then we will only include nodes with passing
|
||||
// health checks (critical AND warning checks will cause a node to be
|
||||
// discarded)
|
||||
OnlyPassing bool
|
||||
|
||||
// Tags are a set of required and/or disallowed tags. If a tag is in
|
||||
// this list it must be present. If the tag is preceded with "!" then
|
||||
// it is disallowed.
|
||||
Tags []string
|
||||
|
||||
// NodeMeta is a map of required node metadata fields. If a key/value
|
||||
// pair is in this map it must be present on the node in order for the
|
||||
// service entry to be returned.
|
||||
NodeMeta map[string]string
|
||||
|
||||
// ServiceMeta is a map of required service metadata fields. If a key/value
|
||||
// pair is in this map it must be present on the node in order for the
|
||||
// service entry to be returned.
|
||||
ServiceMeta map[string]string
|
||||
|
||||
// Connect if true will filter the prepared query results to only
|
||||
// include Connect-capable services. These include both native services
|
||||
// and proxies for matching services. Note that if a proxy matches,
|
||||
// the constraints in the query above (Near, OnlyPassing, etc.) apply
|
||||
// to the _proxy_ and not the service being proxied. In practice, proxies
|
||||
// should be directly next to their services so this isn't an issue.
|
||||
Connect bool
|
||||
}
|
||||
|
||||
// QueryTemplate carries the arguments for creating a templated query.
|
||||
type QueryTemplate struct {
|
||||
// Type specifies the type of the query template. Currently only
|
||||
// "name_prefix_match" is supported. This field is required.
|
||||
Type string
|
||||
|
||||
// Regexp allows specifying a regex pattern to match against the name
|
||||
// of the query being executed.
|
||||
Regexp string
|
||||
}
|
||||
|
||||
// PreparedQueryDefinition defines a complete prepared query.
|
||||
type PreparedQueryDefinition struct {
|
||||
// ID is this UUID-based ID for the query, always generated by Consul.
|
||||
ID string
|
||||
|
||||
// Name is an optional friendly name for the query supplied by the
|
||||
// user. NOTE - if this feature is used then it will reduce the security
|
||||
// of any read ACL associated with this query/service since this name
|
||||
// can be used to locate nodes with supplying any ACL.
|
||||
Name string
|
||||
|
||||
// Session is an optional session to tie this query's lifetime to. If
|
||||
// this is omitted then the query will not expire.
|
||||
Session string
|
||||
|
||||
// Token is the ACL token used when the query was created, and it is
|
||||
// used when a query is subsequently executed. This token, or a token
|
||||
// with management privileges, must be used to change the query later.
|
||||
Token string
|
||||
|
||||
// Service defines a service query (leaving things open for other types
|
||||
// later).
|
||||
Service ServiceQuery
|
||||
|
||||
// DNS has options that control how the results of this query are
|
||||
// served over DNS.
|
||||
DNS QueryDNSOptions
|
||||
|
||||
// Template is used to pass through the arguments for creating a
|
||||
// prepared query with an attached template. If a template is given,
|
||||
// interpolations are possible in other struct fields.
|
||||
Template QueryTemplate
|
||||
}
|
||||
|
||||
// PreparedQueryExecuteResponse has the results of executing a query.
|
||||
type PreparedQueryExecuteResponse struct {
|
||||
// Service is the service that was queried.
|
||||
Service string
|
||||
|
||||
// Namespace of the service that was queried
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// Nodes has the nodes that were output by the query.
|
||||
Nodes []ServiceEntry
|
||||
|
||||
// DNS has the options for serving these results over DNS.
|
||||
DNS QueryDNSOptions
|
||||
|
||||
// Datacenter is the datacenter that these results came from.
|
||||
Datacenter string
|
||||
|
||||
// Failovers is a count of how many times we had to query a remote
|
||||
// datacenter.
|
||||
Failovers int
|
||||
}
|
||||
|
||||
// PreparedQuery can be used to query the prepared query endpoints.
|
||||
type PreparedQuery struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// PreparedQuery returns a handle to the prepared query endpoints.
|
||||
func (c *Client) PreparedQuery() *PreparedQuery {
|
||||
return &PreparedQuery{c}
|
||||
}
|
||||
|
||||
// Create makes a new prepared query. The ID of the new query is returned.
|
||||
func (c *PreparedQuery) Create(query *PreparedQueryDefinition, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
r := c.c.newRequest("POST", "/v1/query")
|
||||
r.setWriteOptions(q)
|
||||
r.obj = query
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
|
||||
var out struct{ ID string }
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// Update makes updates to an existing prepared query.
|
||||
func (c *PreparedQuery) Update(query *PreparedQueryDefinition, q *WriteOptions) (*WriteMeta, error) {
|
||||
return c.c.write("/v1/query/"+query.ID, query, nil, q)
|
||||
}
|
||||
|
||||
// List is used to fetch all the prepared queries (always requires a management
|
||||
// token).
|
||||
func (c *PreparedQuery) List(q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) {
|
||||
var out []*PreparedQueryDefinition
|
||||
qm, err := c.c.query("/v1/query", &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Get is used to fetch a specific prepared query.
|
||||
func (c *PreparedQuery) Get(queryID string, q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) {
|
||||
var out []*PreparedQueryDefinition
|
||||
qm, err := c.c.query("/v1/query/"+queryID, &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete a specific prepared query.
|
||||
func (c *PreparedQuery) Delete(queryID string, q *WriteOptions) (*WriteMeta, error) {
|
||||
r := c.c.newRequest("DELETE", "/v1/query/"+queryID)
|
||||
r.setWriteOptions(q)
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{}
|
||||
wm.RequestTime = rtt
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Execute is used to execute a specific prepared query. You can execute using
|
||||
// a query ID or name.
|
||||
func (c *PreparedQuery) Execute(queryIDOrName string, q *QueryOptions) (*PreparedQueryExecuteResponse, *QueryMeta, error) {
|
||||
var out *PreparedQueryExecuteResponse
|
||||
qm, err := c.c.query("/v1/query/"+queryIDOrName+"/execute", &out, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return out, qm, nil
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package api
|
||||
|
||||
// Raw can be used to do raw queries against custom endpoints
|
||||
type Raw struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Raw returns a handle to query endpoints
|
||||
func (c *Client) Raw() *Raw {
|
||||
return &Raw{c}
|
||||
}
|
||||
|
||||
// Query is used to do a GET request against an endpoint
|
||||
// and deserialize the response into an interface using
|
||||
// standard Consul conventions.
|
||||
func (raw *Raw) Query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
|
||||
return raw.c.query(endpoint, out, q)
|
||||
}
|
||||
|
||||
// Write is used to do a PUT request against an endpoint
|
||||
// and serialize/deserialized using the standard Consul conventions.
|
||||
func (raw *Raw) Write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
|
||||
return raw.c.write(endpoint, in, out, q)
|
||||
}
|
|
@ -0,0 +1,530 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSemaphoreSessionName is the Session Name we assign if none is provided
|
||||
DefaultSemaphoreSessionName = "Consul API Semaphore"
|
||||
|
||||
// DefaultSemaphoreSessionTTL is the default session TTL if no Session is provided
|
||||
// when creating a new Semaphore. This is used because we do not have another
|
||||
// other check to depend upon.
|
||||
DefaultSemaphoreSessionTTL = "15s"
|
||||
|
||||
// DefaultSemaphoreWaitTime is how long we block for at a time to check if semaphore
|
||||
// acquisition is possible. This affects the minimum time it takes to cancel
|
||||
// a Semaphore acquisition.
|
||||
DefaultSemaphoreWaitTime = 15 * time.Second
|
||||
|
||||
// DefaultSemaphoreKey is the key used within the prefix to
|
||||
// use for coordination between all the contenders.
|
||||
DefaultSemaphoreKey = ".lock"
|
||||
|
||||
// SemaphoreFlagValue is a magic flag we set to indicate a key
|
||||
// is being used for a semaphore. It is used to detect a potential
|
||||
// conflict with a lock.
|
||||
SemaphoreFlagValue = 0xe0f69a2baa414de0
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSemaphoreHeld is returned if we attempt to double lock
|
||||
ErrSemaphoreHeld = fmt.Errorf("Semaphore already held")
|
||||
|
||||
// ErrSemaphoreNotHeld is returned if we attempt to unlock a semaphore
|
||||
// that we do not hold.
|
||||
ErrSemaphoreNotHeld = fmt.Errorf("Semaphore not held")
|
||||
|
||||
// ErrSemaphoreInUse is returned if we attempt to destroy a semaphore
|
||||
// that is in use.
|
||||
ErrSemaphoreInUse = fmt.Errorf("Semaphore in use")
|
||||
|
||||
// ErrSemaphoreConflict is returned if the flags on a key
|
||||
// used for a semaphore do not match expectation
|
||||
ErrSemaphoreConflict = fmt.Errorf("Existing key does not match semaphore use")
|
||||
)
|
||||
|
||||
// Semaphore is used to implement a distributed semaphore
|
||||
// using the Consul KV primitives.
|
||||
type Semaphore struct {
|
||||
c *Client
|
||||
opts *SemaphoreOptions
|
||||
|
||||
isHeld bool
|
||||
sessionRenew chan struct{}
|
||||
lockSession string
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
// SemaphoreOptions is used to parameterize the Semaphore
|
||||
type SemaphoreOptions struct {
|
||||
Prefix string // Must be set and have write permissions
|
||||
Limit int // Must be set, and be positive
|
||||
Value []byte // Optional, value to associate with the contender entry
|
||||
Session string // Optional, created if not specified
|
||||
SessionName string // Optional, defaults to DefaultLockSessionName
|
||||
SessionTTL string // Optional, defaults to DefaultLockSessionTTL
|
||||
MonitorRetries int // Optional, defaults to 0 which means no retries
|
||||
MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime
|
||||
SemaphoreWaitTime time.Duration // Optional, defaults to DefaultSemaphoreWaitTime
|
||||
SemaphoreTryOnce bool // Optional, defaults to false which means try forever
|
||||
Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace
|
||||
}
|
||||
|
||||
// semaphoreLock is written under the DefaultSemaphoreKey and
|
||||
// is used to coordinate between all the contenders.
|
||||
type semaphoreLock struct {
|
||||
// Limit is the integer limit of holders. This is used to
|
||||
// verify that all the holders agree on the value.
|
||||
Limit int
|
||||
|
||||
// Holders is a list of all the semaphore holders.
|
||||
// It maps the session ID to true. It is used as a set effectively.
|
||||
Holders map[string]bool
|
||||
}
|
||||
|
||||
// SemaphorePrefix is used to created a Semaphore which will operate
|
||||
// at the given KV prefix and uses the given limit for the semaphore.
|
||||
// The prefix must have write privileges, and the limit must be agreed
|
||||
// upon by all contenders.
|
||||
func (c *Client) SemaphorePrefix(prefix string, limit int) (*Semaphore, error) {
|
||||
opts := &SemaphoreOptions{
|
||||
Prefix: prefix,
|
||||
Limit: limit,
|
||||
}
|
||||
return c.SemaphoreOpts(opts)
|
||||
}
|
||||
|
||||
// SemaphoreOpts is used to create a Semaphore with the given options.
|
||||
// The prefix must have write privileges, and the limit must be agreed
|
||||
// upon by all contenders. If a Session is not provided, one will be created.
|
||||
func (c *Client) SemaphoreOpts(opts *SemaphoreOptions) (*Semaphore, error) {
|
||||
if opts.Prefix == "" {
|
||||
return nil, fmt.Errorf("missing prefix")
|
||||
}
|
||||
if opts.Limit <= 0 {
|
||||
return nil, fmt.Errorf("semaphore limit must be positive")
|
||||
}
|
||||
if opts.SessionName == "" {
|
||||
opts.SessionName = DefaultSemaphoreSessionName
|
||||
}
|
||||
if opts.SessionTTL == "" {
|
||||
opts.SessionTTL = DefaultSemaphoreSessionTTL
|
||||
} else {
|
||||
if _, err := time.ParseDuration(opts.SessionTTL); err != nil {
|
||||
return nil, fmt.Errorf("invalid SessionTTL: %v", err)
|
||||
}
|
||||
}
|
||||
if opts.MonitorRetryTime == 0 {
|
||||
opts.MonitorRetryTime = DefaultMonitorRetryTime
|
||||
}
|
||||
if opts.SemaphoreWaitTime == 0 {
|
||||
opts.SemaphoreWaitTime = DefaultSemaphoreWaitTime
|
||||
}
|
||||
s := &Semaphore{
|
||||
c: c,
|
||||
opts: opts,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Acquire attempts to reserve a slot in the semaphore, blocking until
|
||||
// success, interrupted via the stopCh or an error is encountered.
|
||||
// Providing a non-nil stopCh can be used to abort the attempt.
|
||||
// On success, a channel is returned that represents our slot.
|
||||
// This channel could be closed at any time due to session invalidation,
|
||||
// communication errors, operator intervention, etc. It is NOT safe to
|
||||
// assume that the slot is held until Release() unless the Session is specifically
|
||||
// created without any associated health checks. By default Consul sessions
|
||||
// prefer liveness over safety and an application must be able to handle
|
||||
// the session being lost.
|
||||
func (s *Semaphore) Acquire(stopCh <-chan struct{}) (<-chan struct{}, error) {
|
||||
// Hold the lock as we try to acquire
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
// Check if we already hold the semaphore
|
||||
if s.isHeld {
|
||||
return nil, ErrSemaphoreHeld
|
||||
}
|
||||
|
||||
// Check if we need to create a session first
|
||||
s.lockSession = s.opts.Session
|
||||
if s.lockSession == "" {
|
||||
sess, err := s.createSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session: %v", err)
|
||||
}
|
||||
|
||||
s.sessionRenew = make(chan struct{})
|
||||
s.lockSession = sess
|
||||
session := s.c.Session()
|
||||
go session.RenewPeriodic(s.opts.SessionTTL, sess, nil, s.sessionRenew)
|
||||
|
||||
// If we fail to acquire the lock, cleanup the session
|
||||
defer func() {
|
||||
if !s.isHeld {
|
||||
close(s.sessionRenew)
|
||||
s.sessionRenew = nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Create the contender entry
|
||||
kv := s.c.KV()
|
||||
wOpts := WriteOptions{Namespace: s.opts.Namespace}
|
||||
|
||||
made, _, err := kv.Acquire(s.contenderEntry(s.lockSession), &wOpts)
|
||||
if err != nil || !made {
|
||||
return nil, fmt.Errorf("failed to make contender entry: %v", err)
|
||||
}
|
||||
|
||||
// Setup the query options
|
||||
qOpts := QueryOptions{
|
||||
WaitTime: s.opts.SemaphoreWaitTime,
|
||||
Namespace: s.opts.Namespace,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
attempts := 0
|
||||
WAIT:
|
||||
// Check if we should quit
|
||||
select {
|
||||
case <-stopCh:
|
||||
return nil, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Handle the one-shot mode.
|
||||
if s.opts.SemaphoreTryOnce && attempts > 0 {
|
||||
elapsed := time.Since(start)
|
||||
if elapsed > s.opts.SemaphoreWaitTime {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Query wait time should not exceed the semaphore wait time
|
||||
qOpts.WaitTime = s.opts.SemaphoreWaitTime - elapsed
|
||||
}
|
||||
attempts++
|
||||
|
||||
// Read the prefix
|
||||
pairs, meta, err := kv.List(s.opts.Prefix, &qOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prefix: %v", err)
|
||||
}
|
||||
|
||||
// Decode the lock
|
||||
lockPair := s.findLock(pairs)
|
||||
if lockPair.Flags != SemaphoreFlagValue {
|
||||
return nil, ErrSemaphoreConflict
|
||||
}
|
||||
lock, err := s.decodeLock(lockPair)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify we agree with the limit
|
||||
if lock.Limit != s.opts.Limit {
|
||||
return nil, fmt.Errorf("semaphore limit conflict (lock: %d, local: %d)",
|
||||
lock.Limit, s.opts.Limit)
|
||||
}
|
||||
|
||||
// Prune the dead holders
|
||||
s.pruneDeadHolders(lock, pairs)
|
||||
|
||||
// Check if the lock is held
|
||||
if len(lock.Holders) >= lock.Limit {
|
||||
qOpts.WaitIndex = meta.LastIndex
|
||||
goto WAIT
|
||||
}
|
||||
|
||||
// Create a new lock with us as a holder
|
||||
lock.Holders[s.lockSession] = true
|
||||
newLock, err := s.encodeLock(lock, lockPair.ModifyIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attempt the acquisition
|
||||
didSet, _, err := kv.CAS(newLock, &wOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update lock: %v", err)
|
||||
}
|
||||
if !didSet {
|
||||
// Update failed, could have been a race with another contender,
|
||||
// retry the operation
|
||||
goto WAIT
|
||||
}
|
||||
|
||||
// Watch to ensure we maintain ownership of the slot
|
||||
lockCh := make(chan struct{})
|
||||
go s.monitorLock(s.lockSession, lockCh)
|
||||
|
||||
// Set that we own the lock
|
||||
s.isHeld = true
|
||||
|
||||
// Acquired! All done
|
||||
return lockCh, nil
|
||||
}
|
||||
|
||||
// Release is used to voluntarily give up our semaphore slot. It is
|
||||
// an error to call this if the semaphore has not been acquired.
|
||||
func (s *Semaphore) Release() error {
|
||||
// Hold the lock as we try to release
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
// Ensure the lock is actually held
|
||||
if !s.isHeld {
|
||||
return ErrSemaphoreNotHeld
|
||||
}
|
||||
|
||||
// Set that we no longer own the lock
|
||||
s.isHeld = false
|
||||
|
||||
// Stop the session renew
|
||||
if s.sessionRenew != nil {
|
||||
defer func() {
|
||||
close(s.sessionRenew)
|
||||
s.sessionRenew = nil
|
||||
}()
|
||||
}
|
||||
|
||||
// Get and clear the lock session
|
||||
lockSession := s.lockSession
|
||||
s.lockSession = ""
|
||||
|
||||
// Remove ourselves as a lock holder
|
||||
kv := s.c.KV()
|
||||
key := path.Join(s.opts.Prefix, DefaultSemaphoreKey)
|
||||
|
||||
wOpts := WriteOptions{Namespace: s.opts.Namespace}
|
||||
qOpts := QueryOptions{Namespace: s.opts.Namespace}
|
||||
|
||||
READ:
|
||||
pair, _, err := kv.Get(key, &qOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pair == nil {
|
||||
pair = &KVPair{}
|
||||
}
|
||||
lock, err := s.decodeLock(pair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new lock without us as a holder
|
||||
if _, ok := lock.Holders[lockSession]; ok {
|
||||
delete(lock.Holders, lockSession)
|
||||
newLock, err := s.encodeLock(lock, pair.ModifyIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Swap the locks
|
||||
didSet, _, err := kv.CAS(newLock, &wOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update lock: %v", err)
|
||||
}
|
||||
if !didSet {
|
||||
goto READ
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy the contender entry
|
||||
contenderKey := path.Join(s.opts.Prefix, lockSession)
|
||||
if _, err := kv.Delete(contenderKey, &wOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy is used to cleanup the semaphore entry. It is not necessary
|
||||
// to invoke. It will fail if the semaphore is in use.
|
||||
func (s *Semaphore) Destroy() error {
|
||||
// Hold the lock as we try to acquire
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
// Check if we already hold the semaphore
|
||||
if s.isHeld {
|
||||
return ErrSemaphoreHeld
|
||||
}
|
||||
|
||||
// List for the semaphore
|
||||
kv := s.c.KV()
|
||||
|
||||
q := QueryOptions{Namespace: s.opts.Namespace}
|
||||
pairs, _, err := kv.List(s.opts.Prefix, &q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read prefix: %v", err)
|
||||
}
|
||||
|
||||
// Find the lock pair, bail if it doesn't exist
|
||||
lockPair := s.findLock(pairs)
|
||||
if lockPair.ModifyIndex == 0 {
|
||||
return nil
|
||||
}
|
||||
if lockPair.Flags != SemaphoreFlagValue {
|
||||
return ErrSemaphoreConflict
|
||||
}
|
||||
|
||||
// Decode the lock
|
||||
lock, err := s.decodeLock(lockPair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prune the dead holders
|
||||
s.pruneDeadHolders(lock, pairs)
|
||||
|
||||
// Check if there are any holders
|
||||
if len(lock.Holders) > 0 {
|
||||
return ErrSemaphoreInUse
|
||||
}
|
||||
|
||||
// Attempt the delete
|
||||
w := WriteOptions{Namespace: s.opts.Namespace}
|
||||
didRemove, _, err := kv.DeleteCAS(lockPair, &w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove semaphore: %v", err)
|
||||
}
|
||||
if !didRemove {
|
||||
return ErrSemaphoreInUse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSession is used to create a new managed session
|
||||
func (s *Semaphore) createSession() (string, error) {
|
||||
session := s.c.Session()
|
||||
se := &SessionEntry{
|
||||
Name: s.opts.SessionName,
|
||||
TTL: s.opts.SessionTTL,
|
||||
Behavior: SessionBehaviorDelete,
|
||||
}
|
||||
|
||||
w := WriteOptions{Namespace: s.opts.Namespace}
|
||||
id, _, err := session.Create(se, &w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// contenderEntry returns a formatted KVPair for the contender
|
||||
func (s *Semaphore) contenderEntry(session string) *KVPair {
|
||||
return &KVPair{
|
||||
Key: path.Join(s.opts.Prefix, session),
|
||||
Value: s.opts.Value,
|
||||
Session: session,
|
||||
Flags: SemaphoreFlagValue,
|
||||
}
|
||||
}
|
||||
|
||||
// findLock is used to find the KV Pair which is used for coordination
|
||||
func (s *Semaphore) findLock(pairs KVPairs) *KVPair {
|
||||
key := path.Join(s.opts.Prefix, DefaultSemaphoreKey)
|
||||
for _, pair := range pairs {
|
||||
if pair.Key == key {
|
||||
return pair
|
||||
}
|
||||
}
|
||||
return &KVPair{Flags: SemaphoreFlagValue}
|
||||
}
|
||||
|
||||
// decodeLock is used to decode a semaphoreLock from an
|
||||
// entry in Consul
|
||||
func (s *Semaphore) decodeLock(pair *KVPair) (*semaphoreLock, error) {
|
||||
// Handle if there is no lock
|
||||
if pair == nil || pair.Value == nil {
|
||||
return &semaphoreLock{
|
||||
Limit: s.opts.Limit,
|
||||
Holders: make(map[string]bool),
|
||||
}, nil
|
||||
}
|
||||
|
||||
l := &semaphoreLock{}
|
||||
if err := json.Unmarshal(pair.Value, l); err != nil {
|
||||
return nil, fmt.Errorf("lock decoding failed: %v", err)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// encodeLock is used to encode a semaphoreLock into a KVPair
|
||||
// that can be PUT
|
||||
func (s *Semaphore) encodeLock(l *semaphoreLock, oldIndex uint64) (*KVPair, error) {
|
||||
enc, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lock encoding failed: %v", err)
|
||||
}
|
||||
pair := &KVPair{
|
||||
Key: path.Join(s.opts.Prefix, DefaultSemaphoreKey),
|
||||
Value: enc,
|
||||
Flags: SemaphoreFlagValue,
|
||||
ModifyIndex: oldIndex,
|
||||
}
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
// pruneDeadHolders is used to remove all the dead lock holders
|
||||
func (s *Semaphore) pruneDeadHolders(lock *semaphoreLock, pairs KVPairs) {
|
||||
// Gather all the live holders
|
||||
alive := make(map[string]struct{}, len(pairs))
|
||||
for _, pair := range pairs {
|
||||
if pair.Session != "" {
|
||||
alive[pair.Session] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any holders that are dead
|
||||
for holder := range lock.Holders {
|
||||
if _, ok := alive[holder]; !ok {
|
||||
delete(lock.Holders, holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLock is a long running routine to monitor a semaphore ownership
|
||||
// It closes the stopCh if we lose our slot.
|
||||
func (s *Semaphore) monitorLock(session string, stopCh chan struct{}) {
|
||||
defer close(stopCh)
|
||||
kv := s.c.KV()
|
||||
opts := QueryOptions{
|
||||
RequireConsistent: true,
|
||||
Namespace: s.opts.Namespace,
|
||||
}
|
||||
WAIT:
|
||||
retries := s.opts.MonitorRetries
|
||||
RETRY:
|
||||
pairs, meta, err := kv.List(s.opts.Prefix, &opts)
|
||||
if err != nil {
|
||||
// If configured we can try to ride out a brief Consul unavailability
|
||||
// by doing retries. Note that we have to attempt the retry in a non-
|
||||
// blocking fashion so that we have a clean place to reset the retry
|
||||
// counter if service is restored.
|
||||
if retries > 0 && IsRetryableError(err) {
|
||||
time.Sleep(s.opts.MonitorRetryTime)
|
||||
retries--
|
||||
opts.WaitIndex = 0
|
||||
goto RETRY
|
||||
}
|
||||
return
|
||||
}
|
||||
lockPair := s.findLock(pairs)
|
||||
lock, err := s.decodeLock(lockPair)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.pruneDeadHolders(lock, pairs)
|
||||
if _, ok := lock.Holders[session]; ok {
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
goto WAIT
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// SessionBehaviorRelease is the default behavior and causes
|
||||
// all associated locks to be released on session invalidation.
|
||||
SessionBehaviorRelease = "release"
|
||||
|
||||
// SessionBehaviorDelete is new in Consul 0.5 and changes the
|
||||
// behavior to delete all associated locks on session invalidation.
|
||||
// It can be used in a way similar to Ephemeral Nodes in ZooKeeper.
|
||||
SessionBehaviorDelete = "delete"
|
||||
)
|
||||
|
||||
var ErrSessionExpired = errors.New("session expired")
|
||||
|
||||
// SessionEntry represents a session in consul
|
||||
type SessionEntry struct {
|
||||
CreateIndex uint64
|
||||
ID string
|
||||
Name string
|
||||
Node string
|
||||
LockDelay time.Duration
|
||||
Behavior string
|
||||
TTL string
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// Deprecated for Consul Enterprise in v1.7.0.
|
||||
Checks []string
|
||||
|
||||
// NodeChecks and ServiceChecks are new in Consul 1.7.0.
|
||||
// When associating checks with sessions, namespaces can be specified for service checks.
|
||||
NodeChecks []string
|
||||
ServiceChecks []ServiceCheck
|
||||
}
|
||||
|
||||
type ServiceCheck struct {
|
||||
ID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// Session can be used to query the Session endpoints
|
||||
type Session struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Session returns a handle to the session endpoints
|
||||
func (c *Client) Session() *Session {
|
||||
return &Session{c}
|
||||
}
|
||||
|
||||
// CreateNoChecks is like Create but is used specifically to create
|
||||
// a session with no associated health checks.
|
||||
func (s *Session) CreateNoChecks(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
body := make(map[string]interface{})
|
||||
body["NodeChecks"] = []string{}
|
||||
if se != nil {
|
||||
if se.Name != "" {
|
||||
body["Name"] = se.Name
|
||||
}
|
||||
if se.Node != "" {
|
||||
body["Node"] = se.Node
|
||||
}
|
||||
if se.LockDelay != 0 {
|
||||
body["LockDelay"] = durToMsec(se.LockDelay)
|
||||
}
|
||||
if se.Behavior != "" {
|
||||
body["Behavior"] = se.Behavior
|
||||
}
|
||||
if se.TTL != "" {
|
||||
body["TTL"] = se.TTL
|
||||
}
|
||||
}
|
||||
return s.create(body, q)
|
||||
|
||||
}
|
||||
|
||||
// Create makes a new session. Providing a session entry can
|
||||
// customize the session. It can also be nil to use defaults.
|
||||
func (s *Session) Create(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
var obj interface{}
|
||||
if se != nil {
|
||||
body := make(map[string]interface{})
|
||||
obj = body
|
||||
if se.Name != "" {
|
||||
body["Name"] = se.Name
|
||||
}
|
||||
if se.Node != "" {
|
||||
body["Node"] = se.Node
|
||||
}
|
||||
if se.LockDelay != 0 {
|
||||
body["LockDelay"] = durToMsec(se.LockDelay)
|
||||
}
|
||||
if len(se.Checks) > 0 {
|
||||
body["Checks"] = se.Checks
|
||||
}
|
||||
if len(se.NodeChecks) > 0 {
|
||||
body["NodeChecks"] = se.NodeChecks
|
||||
}
|
||||
if len(se.ServiceChecks) > 0 {
|
||||
body["ServiceChecks"] = se.ServiceChecks
|
||||
}
|
||||
if se.Behavior != "" {
|
||||
body["Behavior"] = se.Behavior
|
||||
}
|
||||
if se.TTL != "" {
|
||||
body["TTL"] = se.TTL
|
||||
}
|
||||
}
|
||||
return s.create(obj, q)
|
||||
}
|
||||
|
||||
func (s *Session) create(obj interface{}, q *WriteOptions) (string, *WriteMeta, error) {
|
||||
var out struct{ ID string }
|
||||
wm, err := s.c.write("/v1/session/create", obj, &out, q)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return out.ID, wm, nil
|
||||
}
|
||||
|
||||
// Destroy invalidates a given session
|
||||
func (s *Session) Destroy(id string, q *WriteOptions) (*WriteMeta, error) {
|
||||
wm, err := s.c.write("/v1/session/destroy/"+id, nil, nil, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Renew renews the TTL on a given session
|
||||
func (s *Session) Renew(id string, q *WriteOptions) (*SessionEntry, *WriteMeta, error) {
|
||||
r := s.c.newRequest("PUT", "/v1/session/renew/"+id)
|
||||
r.setWriteOptions(q)
|
||||
rtt, resp, err := s.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, wm, nil
|
||||
} else if resp.StatusCode != 200 {
|
||||
return nil, nil, fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var entries []*SessionEntry
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return entries[0], wm, nil
|
||||
}
|
||||
return nil, wm, nil
|
||||
}
|
||||
|
||||
// RenewPeriodic is used to periodically invoke Session.Renew on a
|
||||
// session until a doneCh is closed. This is meant to be used in a long running
|
||||
// goroutine to ensure a session stays valid.
|
||||
func (s *Session) RenewPeriodic(initialTTL string, id string, q *WriteOptions, doneCh <-chan struct{}) error {
|
||||
ctx := q.Context()
|
||||
|
||||
ttl, err := time.ParseDuration(initialTTL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
waitDur := ttl / 2
|
||||
lastRenewTime := time.Now()
|
||||
var lastErr error
|
||||
for {
|
||||
if time.Since(lastRenewTime) > ttl {
|
||||
return lastErr
|
||||
}
|
||||
select {
|
||||
case <-time.After(waitDur):
|
||||
entry, _, err := s.Renew(id, q)
|
||||
if err != nil {
|
||||
waitDur = time.Second
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if entry == nil {
|
||||
return ErrSessionExpired
|
||||
}
|
||||
|
||||
// Handle the server updating the TTL
|
||||
ttl, _ = time.ParseDuration(entry.TTL)
|
||||
waitDur = ttl / 2
|
||||
lastRenewTime = time.Now()
|
||||
|
||||
case <-doneCh:
|
||||
// Attempt a session destroy
|
||||
s.Destroy(id, q)
|
||||
return nil
|
||||
|
||||
case <-ctx.Done():
|
||||
// Bail immediately since attempting the destroy would
|
||||
// use the canceled context in q, which would just bail.
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info looks up a single session
|
||||
func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) {
|
||||
var entries []*SessionEntry
|
||||
qm, err := s.c.query("/v1/session/info/"+id, &entries, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return entries[0], qm, nil
|
||||
}
|
||||
return nil, qm, nil
|
||||
}
|
||||
|
||||
// List gets sessions for a node
|
||||
func (s *Session) Node(node string, q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) {
|
||||
var entries []*SessionEntry
|
||||
qm, err := s.c.query("/v1/session/node/"+node, &entries, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
// List gets all active sessions
|
||||
func (s *Session) List(q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) {
|
||||
var entries []*SessionEntry
|
||||
qm, err := s.c.query("/v1/session/list", &entries, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Snapshot can be used to query the /v1/snapshot endpoint to take snapshots of
|
||||
// Consul's internal state and restore snapshots for disaster recovery.
|
||||
type Snapshot struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Snapshot returns a handle that exposes the snapshot endpoints.
|
||||
func (c *Client) Snapshot() *Snapshot {
|
||||
return &Snapshot{c}
|
||||
}
|
||||
|
||||
// Save requests a new snapshot and provides an io.ReadCloser with the snapshot
|
||||
// data to save. If this doesn't return an error, then it's the responsibility
|
||||
// of the caller to close it. Only a subset of the QueryOptions are supported:
|
||||
// Datacenter, AllowStale, and Token.
|
||||
func (s *Snapshot) Save(q *QueryOptions) (io.ReadCloser, *QueryMeta, error) {
|
||||
r := s.c.newRequest("GET", "/v1/snapshot")
|
||||
r.setQueryOptions(q)
|
||||
|
||||
rtt, resp, err := requireOK(s.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
return resp.Body, qm, nil
|
||||
}
|
||||
|
||||
// Restore streams in an existing snapshot and attempts to restore it.
|
||||
func (s *Snapshot) Restore(q *WriteOptions, in io.Reader) error {
|
||||
r := s.c.newRequest("PUT", "/v1/snapshot")
|
||||
r.body = in
|
||||
r.setWriteOptions(q)
|
||||
_, _, err := requireOK(s.c.doRequest(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package api
|
||||
|
||||
// Status can be used to query the Status endpoints
|
||||
type Status struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Status returns a handle to the status endpoints
|
||||
func (c *Client) Status() *Status {
|
||||
return &Status{c}
|
||||
}
|
||||
|
||||
// Leader is used to query for a known leader
|
||||
func (s *Status) LeaderWithQueryOptions(q *QueryOptions) (string, error) {
|
||||
r := s.c.newRequest("GET", "/v1/status/leader")
|
||||
|
||||
if q != nil {
|
||||
r.setQueryOptions(q)
|
||||
}
|
||||
|
||||
_, resp, err := requireOK(s.c.doRequest(r))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var leader string
|
||||
if err := decodeBody(resp, &leader); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return leader, nil
|
||||
}
|
||||
|
||||
func (s *Status) Leader() (string, error) {
|
||||
return s.LeaderWithQueryOptions(nil)
|
||||
}
|
||||
|
||||
// Peers is used to query for a known raft peers
|
||||
func (s *Status) PeersWithQueryOptions(q *QueryOptions) ([]string, error) {
|
||||
r := s.c.newRequest("GET", "/v1/status/peers")
|
||||
|
||||
if q != nil {
|
||||
r.setQueryOptions(q)
|
||||
}
|
||||
|
||||
_, resp, err := requireOK(s.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var peers []string
|
||||
if err := decodeBody(resp, &peers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (s *Status) Peers() ([]string, error) {
|
||||
return s.PeersWithQueryOptions(nil)
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Txn is used to manipulate the Txn API
|
||||
type Txn struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Txn is used to return a handle to the K/V apis
|
||||
func (c *Client) Txn() *Txn {
|
||||
return &Txn{c}
|
||||
}
|
||||
|
||||
// TxnOp is the internal format we send to Consul. Currently only K/V and
|
||||
// check operations are supported.
|
||||
type TxnOp struct {
|
||||
KV *KVTxnOp
|
||||
Node *NodeTxnOp
|
||||
Service *ServiceTxnOp
|
||||
Check *CheckTxnOp
|
||||
}
|
||||
|
||||
// TxnOps is a list of transaction operations.
|
||||
type TxnOps []*TxnOp
|
||||
|
||||
// TxnResult is the internal format we receive from Consul.
|
||||
type TxnResult struct {
|
||||
KV *KVPair
|
||||
Node *Node
|
||||
Service *CatalogService
|
||||
Check *HealthCheck
|
||||
}
|
||||
|
||||
// TxnResults is a list of TxnResult objects.
|
||||
type TxnResults []*TxnResult
|
||||
|
||||
// TxnError is used to return information about an operation in a transaction.
|
||||
type TxnError struct {
|
||||
OpIndex int
|
||||
What string
|
||||
}
|
||||
|
||||
// TxnErrors is a list of TxnError objects.
|
||||
type TxnErrors []*TxnError
|
||||
|
||||
// TxnResponse is the internal format we receive from Consul.
|
||||
type TxnResponse struct {
|
||||
Results TxnResults
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// KVOp constants give possible operations available in a transaction.
|
||||
type KVOp string
|
||||
|
||||
const (
|
||||
KVSet KVOp = "set"
|
||||
KVDelete KVOp = "delete"
|
||||
KVDeleteCAS KVOp = "delete-cas"
|
||||
KVDeleteTree KVOp = "delete-tree"
|
||||
KVCAS KVOp = "cas"
|
||||
KVLock KVOp = "lock"
|
||||
KVUnlock KVOp = "unlock"
|
||||
KVGet KVOp = "get"
|
||||
KVGetTree KVOp = "get-tree"
|
||||
KVCheckSession KVOp = "check-session"
|
||||
KVCheckIndex KVOp = "check-index"
|
||||
KVCheckNotExists KVOp = "check-not-exists"
|
||||
)
|
||||
|
||||
// KVTxnOp defines a single operation inside a transaction.
|
||||
type KVTxnOp struct {
|
||||
Verb KVOp
|
||||
Key string
|
||||
Value []byte
|
||||
Flags uint64
|
||||
Index uint64
|
||||
Session string
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// KVTxnOps defines a set of operations to be performed inside a single
|
||||
// transaction.
|
||||
type KVTxnOps []*KVTxnOp
|
||||
|
||||
// KVTxnResponse has the outcome of a transaction.
|
||||
type KVTxnResponse struct {
|
||||
Results []*KVPair
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// SessionOp constants give possible operations available in a transaction.
|
||||
type SessionOp string
|
||||
|
||||
const (
|
||||
SessionDelete SessionOp = "delete"
|
||||
)
|
||||
|
||||
// SessionTxnOp defines a single operation inside a transaction.
|
||||
type SessionTxnOp struct {
|
||||
Verb SessionOp
|
||||
Session Session
|
||||
}
|
||||
|
||||
// NodeOp constants give possible operations available in a transaction.
|
||||
type NodeOp string
|
||||
|
||||
const (
|
||||
NodeGet NodeOp = "get"
|
||||
NodeSet NodeOp = "set"
|
||||
NodeCAS NodeOp = "cas"
|
||||
NodeDelete NodeOp = "delete"
|
||||
NodeDeleteCAS NodeOp = "delete-cas"
|
||||
)
|
||||
|
||||
// NodeTxnOp defines a single operation inside a transaction.
|
||||
type NodeTxnOp struct {
|
||||
Verb NodeOp
|
||||
Node Node
|
||||
}
|
||||
|
||||
// ServiceOp constants give possible operations available in a transaction.
|
||||
type ServiceOp string
|
||||
|
||||
const (
|
||||
ServiceGet ServiceOp = "get"
|
||||
ServiceSet ServiceOp = "set"
|
||||
ServiceCAS ServiceOp = "cas"
|
||||
ServiceDelete ServiceOp = "delete"
|
||||
ServiceDeleteCAS ServiceOp = "delete-cas"
|
||||
)
|
||||
|
||||
// ServiceTxnOp defines a single operation inside a transaction.
|
||||
type ServiceTxnOp struct {
|
||||
Verb ServiceOp
|
||||
Node string
|
||||
Service AgentService
|
||||
}
|
||||
|
||||
// CheckOp constants give possible operations available in a transaction.
|
||||
type CheckOp string
|
||||
|
||||
const (
|
||||
CheckGet CheckOp = "get"
|
||||
CheckSet CheckOp = "set"
|
||||
CheckCAS CheckOp = "cas"
|
||||
CheckDelete CheckOp = "delete"
|
||||
CheckDeleteCAS CheckOp = "delete-cas"
|
||||
)
|
||||
|
||||
// CheckTxnOp defines a single operation inside a transaction.
|
||||
type CheckTxnOp struct {
|
||||
Verb CheckOp
|
||||
Check HealthCheck
|
||||
}
|
||||
|
||||
// Txn is used to apply multiple Consul operations in a single, atomic transaction.
|
||||
//
|
||||
// Note that Go will perform the required base64 encoding on the values
|
||||
// automatically because the type is a byte slice. Transactions are defined as a
|
||||
// list of operations to perform, using the different fields in the TxnOp structure
|
||||
// to define operations. If any operation fails, none of the changes are applied
|
||||
// to the state store.
|
||||
//
|
||||
// Even though this is generally a write operation, we take a QueryOptions input
|
||||
// and return a QueryMeta output. If the transaction contains only read ops, then
|
||||
// Consul will fast-path it to a different endpoint internally which supports
|
||||
// consistency controls, but not blocking. If there are write operations then
|
||||
// the request will always be routed through raft and any consistency settings
|
||||
// will be ignored.
|
||||
//
|
||||
// Here's an example:
|
||||
//
|
||||
// ops := KVTxnOps{
|
||||
// &KVTxnOp{
|
||||
// Verb: KVLock,
|
||||
// Key: "test/lock",
|
||||
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
|
||||
// Value: []byte("hello"),
|
||||
// },
|
||||
// &KVTxnOp{
|
||||
// Verb: KVGet,
|
||||
// Key: "another/key",
|
||||
// },
|
||||
// &CheckTxnOp{
|
||||
// Verb: CheckSet,
|
||||
// HealthCheck: HealthCheck{
|
||||
// Node: "foo",
|
||||
// CheckID: "redis:a",
|
||||
// Name: "Redis Health Check",
|
||||
// Status: "passing",
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// ok, response, _, err := kv.Txn(&ops, nil)
|
||||
//
|
||||
// If there is a problem making the transaction request then an error will be
|
||||
// returned. Otherwise, the ok value will be true if the transaction succeeded
|
||||
// or false if it was rolled back. The response is a structured return value which
|
||||
// will have the outcome of the transaction. Its Results member will have entries
|
||||
// for each operation. For KV operations, Deleted keys will have a nil entry in the
|
||||
// results, and to save space, the Value of each key in the Results will be nil
|
||||
// unless the operation is a KVGet. If the transaction was rolled back, the Errors
|
||||
// member will have entries referencing the index of the operation that failed
|
||||
// along with an error message.
|
||||
func (t *Txn) Txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
|
||||
return t.c.txn(txn, q)
|
||||
}
|
||||
|
||||
func (c *Client) txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
|
||||
r := c.newRequest("PUT", "/v1/txn")
|
||||
r.setQueryOptions(q)
|
||||
|
||||
r.obj = txn
|
||||
rtt, resp, err := c.doRequest(r)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
|
||||
var txnResp TxnResponse
|
||||
if err := decodeBody(resp, &txnResp); err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
return resp.StatusCode == http.StatusOK, &txnResp, qm, nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
package watch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
consulapi "github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
// watchFactory is a function that can create a new WatchFunc
|
||||
// from a parameter configuration
|
||||
type watchFactory func(params map[string]interface{}) (WatcherFunc, error)
|
||||
|
||||
// watchFuncFactory maps each type to a factory function
|
||||
var watchFuncFactory map[string]watchFactory
|
||||
|
||||
func init() {
|
||||
watchFuncFactory = map[string]watchFactory{
|
||||
"key": keyWatch,
|
||||
"keyprefix": keyPrefixWatch,
|
||||
"services": servicesWatch,
|
||||
"nodes": nodesWatch,
|
||||
"service": serviceWatch,
|
||||
"checks": checksWatch,
|
||||
"event": eventWatch,
|
||||
"connect_roots": connectRootsWatch,
|
||||
"connect_leaf": connectLeafWatch,
|
||||
"agent_service": agentServiceWatch,
|
||||
}
|
||||
}
|
||||
|
||||
// keyWatch is used to return a key watching function
|
||||
func keyWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var key string
|
||||
if err := assignValue(params, "key", &key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("Must specify a single key to watch")
|
||||
}
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
kv := p.client.KV()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
pair, meta, err := kv.Get(key, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if pair == nil {
|
||||
return WaitIndexVal(meta.LastIndex), nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), pair, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// keyPrefixWatch is used to return a key prefix watching function
|
||||
func keyPrefixWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if err := assignValue(params, "prefix", &prefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prefix == "" {
|
||||
return nil, fmt.Errorf("Must specify a single prefix to watch")
|
||||
}
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
kv := p.client.KV()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
pairs, meta, err := kv.List(prefix, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), pairs, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// servicesWatch is used to watch the list of available services
|
||||
func servicesWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
catalog := p.client.Catalog()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
services, meta, err := catalog.Services(&opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), services, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// nodesWatch is used to watch the list of available nodes
|
||||
func nodesWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
catalog := p.client.Catalog()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
nodes, meta, err := catalog.Nodes(&opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), nodes, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// serviceWatch is used to watch a specific service for changes
|
||||
func serviceWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
service string
|
||||
tags []string
|
||||
)
|
||||
if err := assignValue(params, "service", &service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if service == "" {
|
||||
return nil, fmt.Errorf("Must specify a single service to watch")
|
||||
}
|
||||
if err := assignValueStringSlice(params, "tag", &tags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passingOnly := false
|
||||
if err := assignValueBool(params, "passingonly", &passingOnly); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
health := p.client.Health()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
nodes, meta, err := health.ServiceMultipleTags(service, tags, passingOnly, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), nodes, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// checksWatch is used to watch a specific checks in a given state
|
||||
func checksWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
stale := false
|
||||
if err := assignValueBool(params, "stale", &stale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var service, state string
|
||||
if err := assignValue(params, "service", &service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assignValue(params, "state", &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if service != "" && state != "" {
|
||||
return nil, fmt.Errorf("Cannot specify service and state")
|
||||
}
|
||||
if service == "" && state == "" {
|
||||
state = "any"
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
health := p.client.Health()
|
||||
opts := makeQueryOptionsWithContext(p, stale)
|
||||
defer p.cancelFunc()
|
||||
var checks []*consulapi.HealthCheck
|
||||
var meta *consulapi.QueryMeta
|
||||
var err error
|
||||
if state != "" {
|
||||
checks, meta, err = health.State(state, &opts)
|
||||
} else {
|
||||
checks, meta, err = health.Checks(service, &opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), checks, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// eventWatch is used to watch for events, optionally filtering on name
|
||||
func eventWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
// The stale setting doesn't apply to events.
|
||||
|
||||
var name string
|
||||
if err := assignValue(params, "name", &name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
event := p.client.Event()
|
||||
opts := makeQueryOptionsWithContext(p, false)
|
||||
defer p.cancelFunc()
|
||||
events, meta, err := event.List(name, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Prune to only the new events
|
||||
for i := 0; i < len(events); i++ {
|
||||
if WaitIndexVal(event.IDToIndex(events[i].ID)).Equal(p.lastParamVal) {
|
||||
events = events[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
return WaitIndexVal(meta.LastIndex), events, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// connectRootsWatch is used to watch for changes to Connect Root certificates.
|
||||
func connectRootsWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
// We don't support stale since roots are cached locally in the agent.
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
agent := p.client.Agent()
|
||||
opts := makeQueryOptionsWithContext(p, false)
|
||||
defer p.cancelFunc()
|
||||
|
||||
roots, meta, err := agent.ConnectCARoots(&opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return WaitIndexVal(meta.LastIndex), roots, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// connectLeafWatch is used to watch for changes to Connect Leaf certificates
|
||||
// for given local service id.
|
||||
func connectLeafWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
// We don't support stale since certs are cached locally in the agent.
|
||||
|
||||
var serviceName string
|
||||
if err := assignValue(params, "service", &serviceName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
agent := p.client.Agent()
|
||||
opts := makeQueryOptionsWithContext(p, false)
|
||||
defer p.cancelFunc()
|
||||
|
||||
leaf, meta, err := agent.ConnectCALeaf(serviceName, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return WaitIndexVal(meta.LastIndex), leaf, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// agentServiceWatch is used to watch for changes to a single service instance
|
||||
// on the local agent. Note that this state is agent-local so the watch
|
||||
// mechanism uses `hash` rather than `index` for deciding whether to block.
|
||||
func agentServiceWatch(params map[string]interface{}) (WatcherFunc, error) {
|
||||
// We don't support consistency modes since it's agent local data
|
||||
|
||||
var serviceID string
|
||||
if err := assignValue(params, "service_id", &serviceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
|
||||
agent := p.client.Agent()
|
||||
opts := makeQueryOptionsWithContext(p, false)
|
||||
defer p.cancelFunc()
|
||||
|
||||
svc, _, err := agent.Service(serviceID, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Return string ContentHash since we don't have Raft indexes to block on.
|
||||
return WaitHashVal(svc.ContentHash), svc, err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func makeQueryOptionsWithContext(p *Plan, stale bool) consulapi.QueryOptions {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.setCancelFunc(cancel)
|
||||
opts := consulapi.QueryOptions{AllowStale: stale}
|
||||
switch param := p.lastParamVal.(type) {
|
||||
case WaitIndexVal:
|
||||
opts.WaitIndex = uint64(param)
|
||||
case WaitHashVal:
|
||||
opts.WaitHash = string(param)
|
||||
}
|
||||
return *opts.WithContext(ctx)
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
package watch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
consulapi "github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
const (
|
||||
// retryInterval is the base retry value
|
||||
retryInterval = 5 * time.Second
|
||||
|
||||
// maximum back off time, this is to prevent
|
||||
// exponential runaway
|
||||
maxBackoffTime = 180 * time.Second
|
||||
|
||||
// Name used with hclog Logger. We do not add this to the logging package
|
||||
// because we do not want to pull in the root consul module.
|
||||
watchLoggerName = "watch"
|
||||
)
|
||||
|
||||
func (p *Plan) Run(address string) error {
|
||||
return p.RunWithConfig(address, nil)
|
||||
}
|
||||
|
||||
// Run is used to run a watch plan
|
||||
func (p *Plan) RunWithConfig(address string, conf *consulapi.Config) error {
|
||||
logger := p.Logger
|
||||
if logger == nil {
|
||||
logger = newWatchLogger(p.LogOutput)
|
||||
}
|
||||
|
||||
// Setup the client
|
||||
p.address = address
|
||||
if conf == nil {
|
||||
conf = consulapi.DefaultConfigWithLogger(logger)
|
||||
}
|
||||
conf.Address = address
|
||||
conf.Datacenter = p.Datacenter
|
||||
conf.Token = p.Token
|
||||
client, err := consulapi.NewClient(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect to agent: %v", err)
|
||||
}
|
||||
|
||||
return p.RunWithClientAndHclog(client, logger)
|
||||
}
|
||||
|
||||
// RunWithClientAndLogger runs a watch plan using an external client and
|
||||
// hclog.Logger instance. Using this, the plan's Datacenter, Token and LogOutput
|
||||
// fields are ignored and the passed client is expected to be configured as
|
||||
// needed.
|
||||
func (p *Plan) RunWithClientAndHclog(client *consulapi.Client, logger hclog.Logger) error {
|
||||
var watchLogger hclog.Logger
|
||||
if logger == nil {
|
||||
watchLogger = newWatchLogger(nil)
|
||||
} else {
|
||||
watchLogger = logger.Named(watchLoggerName)
|
||||
}
|
||||
|
||||
p.client = client
|
||||
|
||||
// Loop until we are canceled
|
||||
failures := 0
|
||||
OUTER:
|
||||
for !p.shouldStop() {
|
||||
// Invoke the handler
|
||||
blockParamVal, result, err := p.Watcher(p)
|
||||
|
||||
// Check if we should terminate since the function
|
||||
// could have blocked for a while
|
||||
if p.shouldStop() {
|
||||
break
|
||||
}
|
||||
|
||||
// Handle an error in the watch function
|
||||
if err != nil {
|
||||
// Perform an exponential backoff
|
||||
failures++
|
||||
if blockParamVal == nil {
|
||||
p.lastParamVal = nil
|
||||
} else {
|
||||
p.lastParamVal = blockParamVal.Next(p.lastParamVal)
|
||||
}
|
||||
retry := retryInterval * time.Duration(failures*failures)
|
||||
if retry > maxBackoffTime {
|
||||
retry = maxBackoffTime
|
||||
}
|
||||
watchLogger.Error("Watch errored", "type", p.Type, "error", err, "retry", retry)
|
||||
select {
|
||||
case <-time.After(retry):
|
||||
continue OUTER
|
||||
case <-p.stopCh:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the failures
|
||||
failures = 0
|
||||
|
||||
// If the index is unchanged do nothing
|
||||
if p.lastParamVal != nil && p.lastParamVal.Equal(blockParamVal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the index, look for change
|
||||
oldParamVal := p.lastParamVal
|
||||
p.lastParamVal = blockParamVal.Next(oldParamVal)
|
||||
if oldParamVal != nil && reflect.DeepEqual(p.lastResult, result) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the updated result
|
||||
p.lastResult = result
|
||||
// If a hybrid handler exists use that
|
||||
if p.HybridHandler != nil {
|
||||
p.HybridHandler(blockParamVal, result)
|
||||
} else if p.Handler != nil {
|
||||
idx, ok := blockParamVal.(WaitIndexVal)
|
||||
if !ok {
|
||||
watchLogger.Error("Handler only supports index-based " +
|
||||
" watches but non index-based watch run. Skipping Handler.")
|
||||
}
|
||||
p.Handler(uint64(idx), result)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Deprecated: Use RunwithClientAndHclog
|
||||
func (p *Plan) RunWithClientAndLogger(client *consulapi.Client, logger *log.Logger) error {
|
||||
|
||||
p.client = client
|
||||
|
||||
// Loop until we are canceled
|
||||
failures := 0
|
||||
OUTER:
|
||||
for !p.shouldStop() {
|
||||
// Invoke the handler
|
||||
blockParamVal, result, err := p.Watcher(p)
|
||||
|
||||
// Check if we should terminate since the function
|
||||
// could have blocked for a while
|
||||
if p.shouldStop() {
|
||||
break
|
||||
}
|
||||
|
||||
// Handle an error in the watch function
|
||||
if err != nil {
|
||||
// Perform an exponential backoff
|
||||
failures++
|
||||
if blockParamVal == nil {
|
||||
p.lastParamVal = nil
|
||||
} else {
|
||||
p.lastParamVal = blockParamVal.Next(p.lastParamVal)
|
||||
}
|
||||
retry := retryInterval * time.Duration(failures*failures)
|
||||
if retry > maxBackoffTime {
|
||||
retry = maxBackoffTime
|
||||
}
|
||||
logger.Printf("[ERR] consul.watch: Watch (type: %s) errored: %v, retry in %v",
|
||||
p.Type, err, retry)
|
||||
select {
|
||||
case <-time.After(retry):
|
||||
continue OUTER
|
||||
case <-p.stopCh:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the failures
|
||||
failures = 0
|
||||
|
||||
// If the index is unchanged do nothing
|
||||
if p.lastParamVal != nil && p.lastParamVal.Equal(blockParamVal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the index, look for change
|
||||
oldParamVal := p.lastParamVal
|
||||
p.lastParamVal = blockParamVal.Next(oldParamVal)
|
||||
if oldParamVal != nil && reflect.DeepEqual(p.lastResult, result) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the updated result
|
||||
p.lastResult = result
|
||||
// If a hybrid handler exists use that
|
||||
if p.HybridHandler != nil {
|
||||
p.HybridHandler(blockParamVal, result)
|
||||
} else if p.Handler != nil {
|
||||
idx, ok := blockParamVal.(WaitIndexVal)
|
||||
if !ok {
|
||||
logger.Printf("[ERR] consul.watch: Handler only supports index-based " +
|
||||
" watches but non index-based watch run. Skipping Handler.")
|
||||
}
|
||||
p.Handler(uint64(idx), result)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop is used to stop running the watch plan
|
||||
func (p *Plan) Stop() {
|
||||
p.stopLock.Lock()
|
||||
defer p.stopLock.Unlock()
|
||||
if p.stop {
|
||||
return
|
||||
}
|
||||
p.stop = true
|
||||
if p.cancelFunc != nil {
|
||||
p.cancelFunc()
|
||||
}
|
||||
close(p.stopCh)
|
||||
}
|
||||
|
||||
func (p *Plan) shouldStop() bool {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plan) setCancelFunc(cancel context.CancelFunc) {
|
||||
p.stopLock.Lock()
|
||||
defer p.stopLock.Unlock()
|
||||
if p.shouldStop() {
|
||||
// The watch is stopped and execute the new cancel func to stop watchFactory
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
p.cancelFunc = cancel
|
||||
}
|
||||
|
||||
func (p *Plan) IsStopped() bool {
|
||||
p.stopLock.Lock()
|
||||
defer p.stopLock.Unlock()
|
||||
return p.stop
|
||||
}
|
||||
|
||||
func newWatchLogger(output io.Writer) hclog.Logger {
|
||||
return hclog.New(&hclog.LoggerOptions{
|
||||
Name: watchLoggerName,
|
||||
Output: output,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
package watch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
consulapi "github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 10 * time.Second
|
||||
|
||||
// Plan is the parsed version of a watch specification. A watch provides
|
||||
// the details of a query, which generates a view into the Consul data store.
|
||||
// This view is watched for changes and a handler is invoked to take any
|
||||
// appropriate actions.
|
||||
type Plan struct {
|
||||
Datacenter string
|
||||
Token string
|
||||
Type string
|
||||
HandlerType string
|
||||
Exempt map[string]interface{}
|
||||
|
||||
Watcher WatcherFunc
|
||||
// Handler is kept for backward compatibility but only supports watches based
|
||||
// on index param. To support hash based watches, set HybridHandler instead.
|
||||
Handler HandlerFunc
|
||||
HybridHandler HybridHandlerFunc
|
||||
|
||||
Logger hclog.Logger
|
||||
// Deprecated: use Logger
|
||||
LogOutput io.Writer
|
||||
|
||||
address string
|
||||
client *consulapi.Client
|
||||
lastParamVal BlockingParamVal
|
||||
lastResult interface{}
|
||||
|
||||
stop bool
|
||||
stopCh chan struct{}
|
||||
stopLock sync.Mutex
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
type HttpHandlerConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
Method string `mapstructure:"method"`
|
||||
Timeout time.Duration `mapstructure:"-"`
|
||||
TimeoutRaw string `mapstructure:"timeout"`
|
||||
Header map[string][]string `mapstructure:"header"`
|
||||
TLSSkipVerify bool `mapstructure:"tls_skip_verify"`
|
||||
}
|
||||
|
||||
// BlockingParamVal is an interface representing the common operations needed for
|
||||
// different styles of blocking. It's used to abstract the core watch plan from
|
||||
// whether we are performing index-based or hash-based blocking.
|
||||
type BlockingParamVal interface {
|
||||
// Equal returns whether the other param value should be considered equal
|
||||
// (i.e. representing no change in the watched resource). Equal must not panic
|
||||
// if other is nil.
|
||||
Equal(other BlockingParamVal) bool
|
||||
|
||||
// Next is called when deciding which value to use on the next blocking call.
|
||||
// It assumes the BlockingParamVal value it is called on is the most recent one
|
||||
// returned and passes the previous one which may be nil as context. This
|
||||
// allows types to customize logic around ordering without assuming there is
|
||||
// an order. For example WaitIndexVal can check that the index didn't go
|
||||
// backwards and if it did then reset to 0. Most other cases should just
|
||||
// return themselves (the most recent value) to be used in the next request.
|
||||
Next(previous BlockingParamVal) BlockingParamVal
|
||||
}
|
||||
|
||||
// WaitIndexVal is a type representing a Consul index that implements
|
||||
// BlockingParamVal.
|
||||
type WaitIndexVal uint64
|
||||
|
||||
// Equal implements BlockingParamVal
|
||||
func (idx WaitIndexVal) Equal(other BlockingParamVal) bool {
|
||||
if otherIdx, ok := other.(WaitIndexVal); ok {
|
||||
return idx == otherIdx
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Next implements BlockingParamVal
|
||||
func (idx WaitIndexVal) Next(previous BlockingParamVal) BlockingParamVal {
|
||||
if previous == nil {
|
||||
return idx
|
||||
}
|
||||
prevIdx, ok := previous.(WaitIndexVal)
|
||||
if ok && prevIdx == idx {
|
||||
// This value is the same as the previous index, reset
|
||||
return WaitIndexVal(0)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// WaitHashVal is a type representing a Consul content hash that implements
|
||||
// BlockingParamVal.
|
||||
type WaitHashVal string
|
||||
|
||||
// Equal implements BlockingParamVal
|
||||
func (h WaitHashVal) Equal(other BlockingParamVal) bool {
|
||||
if otherHash, ok := other.(WaitHashVal); ok {
|
||||
return h == otherHash
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Next implements BlockingParamVal
|
||||
func (h WaitHashVal) Next(previous BlockingParamVal) BlockingParamVal {
|
||||
return h
|
||||
}
|
||||
|
||||
// WatcherFunc is used to watch for a diff.
|
||||
type WatcherFunc func(*Plan) (BlockingParamVal, interface{}, error)
|
||||
|
||||
// HandlerFunc is used to handle new data. It only works for index-based watches
|
||||
// (which is almost all end points currently) and is kept for backwards
|
||||
// compatibility until more places can make use of hash-based watches too.
|
||||
type HandlerFunc func(uint64, interface{})
|
||||
|
||||
// HybridHandlerFunc is used to handle new data. It can support either
|
||||
// index-based or hash-based watches via the BlockingParamVal.
|
||||
type HybridHandlerFunc func(BlockingParamVal, interface{})
|
||||
|
||||
// Parse takes a watch query and compiles it into a WatchPlan or an error
|
||||
func Parse(params map[string]interface{}) (*Plan, error) {
|
||||
return ParseExempt(params, nil)
|
||||
}
|
||||
|
||||
// ParseExempt takes a watch query and compiles it into a WatchPlan or an error
|
||||
// Any exempt parameters are stored in the Exempt map
|
||||
func ParseExempt(params map[string]interface{}, exempt []string) (*Plan, error) {
|
||||
plan := &Plan{
|
||||
stopCh: make(chan struct{}),
|
||||
Exempt: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// Parse the generic parameters
|
||||
if err := assignValue(params, "datacenter", &plan.Datacenter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assignValue(params, "token", &plan.Token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assignValue(params, "type", &plan.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ensure there is a watch type
|
||||
if plan.Type == "" {
|
||||
return nil, fmt.Errorf("Watch type must be specified")
|
||||
}
|
||||
|
||||
// Get the specific handler
|
||||
if err := assignValue(params, "handler_type", &plan.HandlerType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch plan.HandlerType {
|
||||
case "http":
|
||||
if _, ok := params["http_handler_config"]; !ok {
|
||||
return nil, fmt.Errorf("Handler type 'http' requires 'http_handler_config' to be set")
|
||||
}
|
||||
config, err := parseHttpHandlerConfig(params["http_handler_config"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("Failed to parse 'http_handler_config': %v", err))
|
||||
}
|
||||
plan.Exempt["http_handler_config"] = config
|
||||
delete(params, "http_handler_config")
|
||||
|
||||
case "script":
|
||||
// Let the caller check for configuration in exempt parameters
|
||||
}
|
||||
|
||||
// Look for a factory function
|
||||
factory := watchFuncFactory[plan.Type]
|
||||
if factory == nil {
|
||||
return nil, fmt.Errorf("Unsupported watch type: %s", plan.Type)
|
||||
}
|
||||
|
||||
// Get the watch func
|
||||
fn, err := factory(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan.Watcher = fn
|
||||
|
||||
// Remove the exempt parameters
|
||||
if len(exempt) > 0 {
|
||||
for _, ex := range exempt {
|
||||
val, ok := params[ex]
|
||||
if ok {
|
||||
plan.Exempt[ex] = val
|
||||
delete(params, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all parameters are consumed
|
||||
if len(params) != 0 {
|
||||
var bad []string
|
||||
for key := range params {
|
||||
bad = append(bad, key)
|
||||
}
|
||||
return nil, fmt.Errorf("Invalid parameters: %v", bad)
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// assignValue is used to extract a value ensuring it is a string
|
||||
func assignValue(params map[string]interface{}, name string, out *string) error {
|
||||
if raw, ok := params[name]; ok {
|
||||
val, ok := raw.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expecting %s to be a string", name)
|
||||
}
|
||||
*out = val
|
||||
delete(params, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assignValueBool is used to extract a value ensuring it is a bool
|
||||
func assignValueBool(params map[string]interface{}, name string, out *bool) error {
|
||||
if raw, ok := params[name]; ok {
|
||||
val, ok := raw.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expecting %s to be a boolean", name)
|
||||
}
|
||||
*out = val
|
||||
delete(params, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assignValueStringSlice is used to extract a value ensuring it is either a string or a slice of strings
|
||||
func assignValueStringSlice(params map[string]interface{}, name string, out *[]string) error {
|
||||
if raw, ok := params[name]; ok {
|
||||
var tmp []string
|
||||
switch raw.(type) {
|
||||
case string:
|
||||
tmp = make([]string, 1, 1)
|
||||
tmp[0] = raw.(string)
|
||||
case []string:
|
||||
l := len(raw.([]string))
|
||||
tmp = make([]string, l, l)
|
||||
copy(tmp, raw.([]string))
|
||||
case []interface{}:
|
||||
l := len(raw.([]interface{}))
|
||||
tmp = make([]string, l, l)
|
||||
for i, v := range raw.([]interface{}) {
|
||||
if s, ok := v.(string); ok {
|
||||
tmp[i] = s
|
||||
} else {
|
||||
return fmt.Errorf("Index %d of %s expected to be string", i, name)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Expecting %s to be a string or []string", name)
|
||||
}
|
||||
*out = tmp
|
||||
delete(params, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the 'http_handler_config' parameters
|
||||
func parseHttpHandlerConfig(configParams interface{}) (*HttpHandlerConfig, error) {
|
||||
var config HttpHandlerConfig
|
||||
if err := mapstructure.Decode(configParams, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Path == "" {
|
||||
return nil, fmt.Errorf("Requires 'path' to be set")
|
||||
}
|
||||
if config.Method == "" {
|
||||
config.Method = "POST"
|
||||
}
|
||||
if config.TimeoutRaw == "" {
|
||||
config.Timeout = DefaultTimeout
|
||||
} else if timeout, err := time.ParseDuration(config.TimeoutRaw); err != nil {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("Failed to parse timeout: %v", err))
|
||||
} else {
|
||||
config.Timeout = timeout
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
37
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_darwin.go
generated
vendored
Normal file
37
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_darwin.go
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
//+build darwin
|
||||
|
||||
package freeport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const ephemeralPortRangeSysctlFirst = "net.inet.ip.portrange.first"
|
||||
const ephemeralPortRangeSysctlLast = "net.inet.ip.portrange.last"
|
||||
|
||||
var ephemeralPortRangePatt = regexp.MustCompile(`^\s*(\d+)\s+(\d+)\s*$`)
|
||||
|
||||
func getEphemeralPortRange() (int, int, error) {
|
||||
cmd := exec.Command("/usr/sbin/sysctl", "-n", ephemeralPortRangeSysctlFirst, ephemeralPortRangeSysctlLast)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
val := string(out)
|
||||
|
||||
m := ephemeralPortRangePatt.FindStringSubmatch(val)
|
||||
if m != nil {
|
||||
min, err1 := strconv.Atoi(m[1])
|
||||
max, err2 := strconv.Atoi(m[2])
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
return min, max, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("unexpected sysctl value %q for keys %q, %q", val, ephemeralPortRangeSysctlFirst, ephemeralPortRangeSysctlLast)
|
||||
}
|
7
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_fallback.go
generated
vendored
Normal file
7
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_fallback.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
//+build !linux,!darwin
|
||||
|
||||
package freeport
|
||||
|
||||
func getEphemeralPortRange() (int, int, error) {
|
||||
return 0, 0, nil
|
||||
}
|
36
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_linux.go
generated
vendored
Normal file
36
vendor/github.com/hashicorp/consul/sdk/freeport/ephemeral_linux.go
generated
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
//+build linux
|
||||
|
||||
package freeport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const ephemeralPortRangeSysctlKey = "net.ipv4.ip_local_port_range"
|
||||
|
||||
var ephemeralPortRangePatt = regexp.MustCompile(`^\s*(\d+)\s+(\d+)\s*$`)
|
||||
|
||||
func getEphemeralPortRange() (int, int, error) {
|
||||
cmd := exec.Command("/sbin/sysctl", "-n", ephemeralPortRangeSysctlKey)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
val := string(out)
|
||||
|
||||
m := ephemeralPortRangePatt.FindStringSubmatch(val)
|
||||
if m != nil {
|
||||
min, err1 := strconv.Atoi(m[1])
|
||||
max, err2 := strconv.Atoi(m[2])
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
return min, max, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("unexpected sysctl value %q for key %q", val, ephemeralPortRangeSysctlKey)
|
||||
}
|
|
@ -0,0 +1,391 @@
|
|||
// Package freeport provides a helper for allocating free ports across multiple
|
||||
// processes on the same machine.
|
||||
package freeport
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBlocks is the number of available port blocks before exclusions.
|
||||
maxBlocks = 30
|
||||
|
||||
// lowPort is the lowest port number that should be used.
|
||||
lowPort = 10000
|
||||
|
||||
// attempts is how often we try to allocate a port block
|
||||
// before giving up.
|
||||
attempts = 10
|
||||
)
|
||||
|
||||
var (
|
||||
// blockSize is the size of the allocated port block. ports are given out
|
||||
// consecutively from that block and after that point in a LRU fashion.
|
||||
blockSize int
|
||||
|
||||
// effectiveMaxBlocks is the number of available port blocks.
|
||||
// lowPort + effectiveMaxBlocks * blockSize must be less than 65535.
|
||||
effectiveMaxBlocks int
|
||||
|
||||
// firstPort is the first port of the allocated block.
|
||||
firstPort int
|
||||
|
||||
// lockLn is the system-wide mutex for the port block.
|
||||
lockLn net.Listener
|
||||
|
||||
// mu guards:
|
||||
// - pendingPorts
|
||||
// - freePorts
|
||||
// - total
|
||||
mu sync.Mutex
|
||||
|
||||
// once is used to do the initialization on the first call to retrieve free
|
||||
// ports
|
||||
once sync.Once
|
||||
|
||||
// condNotEmpty is a condition variable to wait for freePorts to be not
|
||||
// empty. Linked to 'mu'
|
||||
condNotEmpty *sync.Cond
|
||||
|
||||
// freePorts is a FIFO of all currently free ports. Take from the front,
|
||||
// and return to the back.
|
||||
freePorts *list.List
|
||||
|
||||
// pendingPorts is a FIFO of recently freed ports that have not yet passed
|
||||
// the not-in-use check.
|
||||
pendingPorts *list.List
|
||||
|
||||
// total is the total number of available ports in the block for use.
|
||||
total int
|
||||
|
||||
// stopCh is used to signal to background goroutines to terminate. Only
|
||||
// really exists for the safety of reset() during unit tests.
|
||||
stopCh chan struct{}
|
||||
|
||||
// stopWg is used to keep track of background goroutines that are still
|
||||
// alive. Only really exists for the safety of reset() during unit tests.
|
||||
stopWg sync.WaitGroup
|
||||
)
|
||||
|
||||
// initialize is used to initialize freeport.
|
||||
func initialize() {
|
||||
var err error
|
||||
|
||||
blockSize = 1500
|
||||
limit, err := systemLimit()
|
||||
if err != nil {
|
||||
panic("freeport: error getting system limit: " + err.Error())
|
||||
}
|
||||
if limit > 0 && limit < blockSize {
|
||||
logf("INFO", "blockSize %d too big for system limit %d. Adjusting...", blockSize, limit)
|
||||
blockSize = limit - 3
|
||||
}
|
||||
|
||||
effectiveMaxBlocks, err = adjustMaxBlocks()
|
||||
if err != nil {
|
||||
panic("freeport: ephemeral port range detection failed: " + err.Error())
|
||||
}
|
||||
if effectiveMaxBlocks < 0 {
|
||||
panic("freeport: no blocks of ports available outside of ephemeral range")
|
||||
}
|
||||
if lowPort+effectiveMaxBlocks*blockSize > 65535 {
|
||||
panic("freeport: block size too big or too many blocks requested")
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
firstPort, lockLn = alloc()
|
||||
|
||||
condNotEmpty = sync.NewCond(&mu)
|
||||
freePorts = list.New()
|
||||
pendingPorts = list.New()
|
||||
|
||||
// fill with all available free ports
|
||||
for port := firstPort + 1; port < firstPort+blockSize; port++ {
|
||||
if used := isPortInUse(port); !used {
|
||||
freePorts.PushBack(port)
|
||||
}
|
||||
}
|
||||
total = freePorts.Len()
|
||||
|
||||
stopWg.Add(1)
|
||||
stopCh = make(chan struct{})
|
||||
// Note: we pass this param explicitly to the goroutine so that we can
|
||||
// freely recreate the underlying stop channel during reset() after closing
|
||||
// the original.
|
||||
go checkFreedPorts(stopCh)
|
||||
}
|
||||
|
||||
func shutdownGoroutine() {
|
||||
mu.Lock()
|
||||
if stopCh == nil {
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
close(stopCh)
|
||||
stopCh = nil
|
||||
mu.Unlock()
|
||||
|
||||
stopWg.Wait()
|
||||
}
|
||||
|
||||
// reset will reverse the setup from initialize() and then redo it (for tests)
|
||||
func reset() {
|
||||
logf("INFO", "resetting the freeport package state")
|
||||
shutdownGoroutine()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
effectiveMaxBlocks = 0
|
||||
firstPort = 0
|
||||
if lockLn != nil {
|
||||
lockLn.Close()
|
||||
lockLn = nil
|
||||
}
|
||||
|
||||
once = sync.Once{}
|
||||
|
||||
freePorts = nil
|
||||
pendingPorts = nil
|
||||
total = 0
|
||||
}
|
||||
|
||||
func checkFreedPorts(stopCh <-chan struct{}) {
|
||||
defer stopWg.Done()
|
||||
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
logf("INFO", "Closing checkFreedPorts()")
|
||||
return
|
||||
case <-ticker.C:
|
||||
checkFreedPortsOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkFreedPortsOnce() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
pending := pendingPorts.Len()
|
||||
remove := make([]*list.Element, 0, pending)
|
||||
for elem := pendingPorts.Front(); elem != nil; elem = elem.Next() {
|
||||
port := elem.Value.(int)
|
||||
if used := isPortInUse(port); !used {
|
||||
freePorts.PushBack(port)
|
||||
remove = append(remove, elem)
|
||||
}
|
||||
}
|
||||
|
||||
retained := pending - len(remove)
|
||||
|
||||
if retained > 0 {
|
||||
logf("WARN", "%d out of %d pending ports are still in use; something probably didn't wait around for the port to be closed!", retained, pending)
|
||||
}
|
||||
|
||||
if len(remove) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, elem := range remove {
|
||||
pendingPorts.Remove(elem)
|
||||
}
|
||||
|
||||
condNotEmpty.Broadcast()
|
||||
}
|
||||
|
||||
// adjustMaxBlocks avoids having the allocation ranges overlap the ephemeral
|
||||
// port range.
|
||||
func adjustMaxBlocks() (int, error) {
|
||||
ephemeralPortMin, ephemeralPortMax, err := getEphemeralPortRange()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if ephemeralPortMin <= 0 || ephemeralPortMax <= 0 {
|
||||
logf("INFO", "ephemeral port range detection not configured for GOOS=%q", runtime.GOOS)
|
||||
return maxBlocks, nil
|
||||
}
|
||||
|
||||
logf("INFO", "detected ephemeral port range of [%d, %d]", ephemeralPortMin, ephemeralPortMax)
|
||||
for block := 0; block < maxBlocks; block++ {
|
||||
min := lowPort + block*blockSize
|
||||
max := min + blockSize
|
||||
overlap := intervalOverlap(min, max-1, ephemeralPortMin, ephemeralPortMax)
|
||||
if overlap {
|
||||
logf("INFO", "reducing max blocks from %d to %d to avoid the ephemeral port range", maxBlocks, block)
|
||||
return block, nil
|
||||
}
|
||||
}
|
||||
return maxBlocks, nil
|
||||
}
|
||||
|
||||
// alloc reserves a port block for exclusive use for the lifetime of the
|
||||
// application. lockLn serves as a system-wide mutex for the port block and is
|
||||
// implemented as a TCP listener which is bound to the firstPort and which will
|
||||
// be automatically released when the application terminates.
|
||||
func alloc() (int, net.Listener) {
|
||||
for i := 0; i < attempts; i++ {
|
||||
block := int(rand.Int31n(int32(effectiveMaxBlocks)))
|
||||
firstPort := lowPort + block*blockSize
|
||||
ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", firstPort))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// logf("DEBUG", "allocated port block %d (%d-%d)", block, firstPort, firstPort+blockSize-1)
|
||||
return firstPort, ln
|
||||
}
|
||||
panic("freeport: cannot allocate port block")
|
||||
}
|
||||
|
||||
// MustTake is the same as Take except it panics on error.
|
||||
func MustTake(n int) (ports []int) {
|
||||
ports, err := Take(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
// Take returns a list of free ports from the allocated port block. It is safe
|
||||
// to call this method concurrently. Ports have been tested to be available on
|
||||
// 127.0.0.1 TCP but there is no guarantee that they will remain free in the
|
||||
// future.
|
||||
func Take(n int) (ports []int, err error) {
|
||||
if n <= 0 {
|
||||
return nil, fmt.Errorf("freeport: cannot take %d ports", n)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Reserve a port block
|
||||
once.Do(initialize)
|
||||
|
||||
if n > total {
|
||||
return nil, fmt.Errorf("freeport: block size too small")
|
||||
}
|
||||
|
||||
for len(ports) < n {
|
||||
for freePorts.Len() == 0 {
|
||||
if total == 0 {
|
||||
return nil, fmt.Errorf("freeport: impossible to satisfy request; there are no actual free ports in the block anymore")
|
||||
}
|
||||
condNotEmpty.Wait()
|
||||
}
|
||||
|
||||
elem := freePorts.Front()
|
||||
freePorts.Remove(elem)
|
||||
port := elem.Value.(int)
|
||||
|
||||
if used := isPortInUse(port); used {
|
||||
// Something outside of the test suite has stolen this port, possibly
|
||||
// due to assignment to an ephemeral port, remove it completely.
|
||||
logf("WARN", "leaked port %d due to theft; removing from circulation", port)
|
||||
total--
|
||||
continue
|
||||
}
|
||||
|
||||
ports = append(ports, port)
|
||||
}
|
||||
|
||||
// logf("DEBUG", "free ports: %v", ports)
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// peekFree returns the next port that will be returned by Take to aid in testing.
|
||||
func peekFree() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return freePorts.Front().Value.(int)
|
||||
}
|
||||
|
||||
// peekAllFree returns all free ports that could be returned by Take to aid in testing.
|
||||
func peekAllFree() []int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
var out []int
|
||||
for elem := freePorts.Front(); elem != nil; elem = elem.Next() {
|
||||
port := elem.Value.(int)
|
||||
out = append(out, port)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// stats returns diagnostic data to aid in testing
|
||||
func stats() (numTotal, numPending, numFree int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return total, pendingPorts.Len(), freePorts.Len()
|
||||
}
|
||||
|
||||
// Return returns a block of ports back to the general pool. These ports should
|
||||
// have been returned from a call to Take().
|
||||
func Return(ports []int) {
|
||||
if len(ports) == 0 {
|
||||
return // convenience short circuit for test ergonomics
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
for _, port := range ports {
|
||||
if port > firstPort && port < firstPort+blockSize {
|
||||
pendingPorts.PushBack(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isPortInUse(port int) bool {
|
||||
ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", port))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
ln.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
func tcpAddr(ip string, port int) *net.TCPAddr {
|
||||
return &net.TCPAddr{IP: net.ParseIP(ip), Port: port}
|
||||
}
|
||||
|
||||
// intervalOverlap returns true if the doubly-inclusive integer intervals
|
||||
// represented by [min1, max1] and [min2, max2] overlap.
|
||||
func intervalOverlap(min1, max1, min2, max2 int) bool {
|
||||
if min1 > max1 {
|
||||
logf("WARN", "interval1 is not ordered [%d, %d]", min1, max1)
|
||||
return false
|
||||
}
|
||||
if min2 > max2 {
|
||||
logf("WARN", "interval2 is not ordered [%d, %d]", min2, max2)
|
||||
return false
|
||||
}
|
||||
return min1 <= max2 && min2 <= max1
|
||||
}
|
||||
|
||||
func logf(severity string, format string, a ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, "["+severity+"] freeport: "+format+"\n", a...)
|
||||
}
|
||||
|
||||
// Deprecated: Please use Take/Return calls instead.
|
||||
func Get(n int) (ports []int) { return MustTake(n) }
|
||||
|
||||
// Deprecated: Please use Take/Return calls instead.
|
||||
func GetT(t testing.T, n int) (ports []int) { return MustTake(n) }
|
||||
|
||||
// Deprecated: Please use Take/Return calls instead.
|
||||
func Free(n int) (ports []int, err error) { return MustTake(n), nil }
|
|
@ -0,0 +1,11 @@
|
|||
// +build !windows
|
||||
|
||||
package freeport
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func systemLimit() (int, error) {
|
||||
var limit unix.Rlimit
|
||||
err := unix.Getrlimit(unix.RLIMIT_NOFILE, &limit)
|
||||
return int(limit.Cur), err
|
||||
}
|
7
vendor/github.com/hashicorp/consul/sdk/freeport/systemlimit_windows.go
generated
vendored
Normal file
7
vendor/github.com/hashicorp/consul/sdk/freeport/systemlimit_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// +build windows
|
||||
|
||||
package freeport
|
||||
|
||||
func systemLimit() (int, error) {
|
||||
return 0, nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
Consul Testing Utilities
|
||||
========================
|
||||
|
||||
This package provides some generic helpers to facilitate testing in Consul.
|
||||
|
||||
TestServer
|
||||
==========
|
||||
|
||||
TestServer is a harness for managing Consul agents and initializing them with
|
||||
test data. Using it, you can form test clusters, create services, add health
|
||||
checks, manipulate the K/V store, etc. This test harness is completely decoupled
|
||||
from Consul's core and API client, meaning it can be easily imported and used in
|
||||
external unit tests for various applications. It works by invoking the Consul
|
||||
CLI, which means it is a requirement to have Consul installed in the `$PATH`.
|
||||
|
||||
Following is an example usage:
|
||||
|
||||
```go
|
||||
package my_program
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
)
|
||||
|
||||
func TestFoo_bar(t *testing.T) {
|
||||
// Create a test Consul server
|
||||
srv1, err := testutil.NewTestServerConfigT(t, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv1.Stop()
|
||||
|
||||
// Create a secondary server, passing in configuration
|
||||
// to avoid bootstrapping as we are forming a cluster.
|
||||
srv2, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) {
|
||||
c.Bootstrap = false
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv2.Stop()
|
||||
|
||||
// Join the servers together
|
||||
srv1.JoinLAN(t, srv2.LANAddr)
|
||||
|
||||
// Create a test key/value pair
|
||||
srv1.SetKV(t, "foo", []byte("bar"))
|
||||
|
||||
// Create lots of test key/value pairs
|
||||
srv1.PopulateKV(t, map[string][]byte{
|
||||
"bar": []byte("123"),
|
||||
"baz": []byte("456"),
|
||||
})
|
||||
|
||||
// Create a service
|
||||
srv1.AddService(t, "redis", structs.HealthPassing, []string{"master"})
|
||||
|
||||
// Create a service that will be accessed in target source code
|
||||
srv1.AddAccessibleService("redis", structs.HealthPassing, "127.0.0.1", 6379, []string{"master"})
|
||||
|
||||
// Create a service check
|
||||
srv1.AddCheck(t, "service:redis", "redis", structs.HealthPassing)
|
||||
|
||||
// Create a node check
|
||||
srv1.AddCheck(t, "mem", "", structs.HealthCritical)
|
||||
|
||||
// The HTTPAddr field contains the address of the Consul
|
||||
// API on the new test server instance.
|
||||
println(srv1.HTTPAddr)
|
||||
|
||||
// All functions also have a wrapper method to limit the passing of "t"
|
||||
wrap := srv1.Wrap(t)
|
||||
wrap.SetKV("foo", []byte("bar"))
|
||||
}
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// RequireErrorContains is a test helper for asserting that an error occurred
|
||||
// and the error message returned contains the expected error message as a
|
||||
// substring.
|
||||
func RequireErrorContains(t *testing.T, err error, expectedErrorMessage string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("An error is expected but got nil.")
|
||||
}
|
||||
if !strings.Contains(err.Error(), expectedErrorMessage) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// tmpdir is the base directory for all temporary directories
|
||||
// and files created with TempDir and TempFile. This could be
|
||||
// achieved by setting a system environment variable but then
|
||||
// the test execution would depend on whether or not the
|
||||
// environment variable is set.
|
||||
//
|
||||
// On macOS the temp base directory is quite long and that
|
||||
// triggers a problem with some tests that bind to UNIX sockets
|
||||
// where the filename seems to be too long. Using a shorter name
|
||||
// fixes this and makes the paths more readable.
|
||||
//
|
||||
// It also provides a single base directory for cleanup.
|
||||
var tmpdir = "/tmp/consul-test"
|
||||
|
||||
func init() {
|
||||
if err := os.MkdirAll(tmpdir, 0755); err != nil {
|
||||
fmt.Printf("Cannot create %s. Reverting to /tmp\n", tmpdir)
|
||||
tmpdir = "/tmp"
|
||||
}
|
||||
}
|
||||
|
||||
var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true"
|
||||
|
||||
// TempDir creates a temporary directory within tmpdir with the name 'testname-name'.
|
||||
// If the directory cannot be created t.Fatal is called.
|
||||
// The directory will be removed when the test ends. Set TEST_NOCLEANUP env var
|
||||
// to prevent the directory from being removed.
|
||||
func TempDir(t *testing.T, name string) string {
|
||||
if t == nil {
|
||||
panic("argument t must be non-nil")
|
||||
}
|
||||
name = t.Name() + "-" + name
|
||||
name = strings.Replace(name, "/", "_", -1)
|
||||
d, err := ioutil.TempDir(tmpdir, name)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if noCleanup {
|
||||
t.Logf("skipping cleanup because TEST_NOCLEANUP was enabled")
|
||||
return
|
||||
}
|
||||
os.RemoveAll(d)
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// TempFile creates a temporary file within tmpdir with the name 'testname-name'.
|
||||
// If the file cannot be created t.Fatal is called. If a temporary directory
|
||||
// has been created before consider storing the file inside this directory to
|
||||
// avoid double cleanup.
|
||||
// The file will be removed when the test ends. Set TEST_NOCLEANUP env var
|
||||
// to prevent the file from being removed.
|
||||
func TempFile(t *testing.T, name string) *os.File {
|
||||
if t == nil {
|
||||
panic("argument t must be non-nil")
|
||||
}
|
||||
name = t.Name() + "-" + name
|
||||
name = strings.Replace(name, "/", "_", -1)
|
||||
f, err := ioutil.TempFile(tmpdir, name)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if noCleanup {
|
||||
t.Logf("skipping cleanup because TEST_NOCLEANUP was enabled")
|
||||
return
|
||||
}
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
return f
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
// Package retry provides support for repeating operations in tests.
|
||||
//
|
||||
// A sample retry operation looks like this:
|
||||
//
|
||||
// func TestX(t *testing.T) {
|
||||
// retry.Run(t, func(r *retry.R) {
|
||||
// if err := foo(); err != nil {
|
||||
// r.Fatal("f: ", err)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
package retry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Failer is an interface compatible with testing.T.
|
||||
type Failer interface {
|
||||
Helper()
|
||||
|
||||
// Log is called for the final test output
|
||||
Log(args ...interface{})
|
||||
|
||||
// FailNow is called when the retrying is abandoned.
|
||||
FailNow()
|
||||
}
|
||||
|
||||
// R provides context for the retryer.
|
||||
type R struct {
|
||||
fail bool
|
||||
output []string
|
||||
}
|
||||
|
||||
func (r *R) FailNow() {
|
||||
r.fail = true
|
||||
runtime.Goexit()
|
||||
}
|
||||
|
||||
func (r *R) Fatal(args ...interface{}) {
|
||||
r.log(fmt.Sprint(args...))
|
||||
r.FailNow()
|
||||
}
|
||||
|
||||
func (r *R) Fatalf(format string, args ...interface{}) {
|
||||
r.log(fmt.Sprintf(format, args...))
|
||||
r.FailNow()
|
||||
}
|
||||
|
||||
func (r *R) Error(args ...interface{}) {
|
||||
r.log(fmt.Sprint(args...))
|
||||
r.fail = true
|
||||
}
|
||||
|
||||
func (r *R) Errorf(format string, args ...interface{}) {
|
||||
r.log(fmt.Sprintf(format, args...))
|
||||
r.fail = true
|
||||
}
|
||||
|
||||
func (r *R) Check(err error) {
|
||||
if err != nil {
|
||||
r.log(err.Error())
|
||||
r.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *R) log(s string) {
|
||||
r.output = append(r.output, decorate(s))
|
||||
}
|
||||
|
||||
func decorate(s string) string {
|
||||
_, file, line, ok := runtime.Caller(3)
|
||||
if ok {
|
||||
n := strings.LastIndex(file, "/")
|
||||
if n >= 0 {
|
||||
file = file[n+1:]
|
||||
}
|
||||
} else {
|
||||
file = "???"
|
||||
line = 1
|
||||
}
|
||||
return fmt.Sprintf("%s:%d: %s", file, line, s)
|
||||
}
|
||||
|
||||
func Run(t Failer, f func(r *R)) {
|
||||
run(DefaultFailer(), t, f)
|
||||
}
|
||||
|
||||
func RunWith(r Retryer, t Failer, f func(r *R)) {
|
||||
run(r, t, f)
|
||||
}
|
||||
|
||||
func dedup(a []string) string {
|
||||
if len(a) == 0 {
|
||||
return ""
|
||||
}
|
||||
m := map[string]int{}
|
||||
for _, s := range a {
|
||||
m[s] = m[s] + 1
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for _, s := range a {
|
||||
if _, ok := m[s]; ok {
|
||||
b.WriteString(s)
|
||||
b.WriteRune('\n')
|
||||
delete(m, s)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func run(r Retryer, t Failer, f func(r *R)) {
|
||||
rr := &R{}
|
||||
fail := func() {
|
||||
t.Helper()
|
||||
out := dedup(rr.output)
|
||||
if out != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
for r.NextOr(fail) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f(rr)
|
||||
}()
|
||||
wg.Wait()
|
||||
if rr.fail {
|
||||
rr.fail = false
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultFailer provides default retry.Run() behavior for unit tests.
|
||||
func DefaultFailer() *Timer {
|
||||
return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond}
|
||||
}
|
||||
|
||||
// TwoSeconds repeats an operation for two seconds and waits 25ms in between.
|
||||
func TwoSeconds() *Timer {
|
||||
return &Timer{Timeout: 2 * time.Second, Wait: 25 * time.Millisecond}
|
||||
}
|
||||
|
||||
// ThreeTimes repeats an operation three times and waits 25ms in between.
|
||||
func ThreeTimes() *Counter {
|
||||
return &Counter{Count: 3, Wait: 25 * time.Millisecond}
|
||||
}
|
||||
|
||||
// Retryer provides an interface for repeating operations
|
||||
// until they succeed or an exit condition is met.
|
||||
type Retryer interface {
|
||||
// NextOr returns true if the operation should be repeated.
|
||||
// Otherwise, it calls fail and returns false.
|
||||
NextOr(fail func()) bool
|
||||
}
|
||||
|
||||
// Counter repeats an operation a given number of
|
||||
// times and waits between subsequent operations.
|
||||
type Counter struct {
|
||||
Count int
|
||||
Wait time.Duration
|
||||
|
||||
count int
|
||||
}
|
||||
|
||||
func (r *Counter) NextOr(fail func()) bool {
|
||||
if r.count == r.Count {
|
||||
fail()
|
||||
return false
|
||||
}
|
||||
if r.count > 0 {
|
||||
time.Sleep(r.Wait)
|
||||
}
|
||||
r.count++
|
||||
return true
|
||||
}
|
||||
|
||||
// Timer repeats an operation for a given amount
|
||||
// of time and waits between subsequent operations.
|
||||
type Timer struct {
|
||||
Timeout time.Duration
|
||||
Wait time.Duration
|
||||
|
||||
// stop is the timeout deadline.
|
||||
// Set on the first invocation of Next().
|
||||
stop time.Time
|
||||
}
|
||||
|
||||
func (r *Timer) NextOr(fail func()) bool {
|
||||
if r.stop.IsZero() {
|
||||
r.stop = time.Now().Add(r.Timeout)
|
||||
return true
|
||||
}
|
||||
if time.Now().After(r.stop) {
|
||||
fail()
|
||||
return false
|
||||
}
|
||||
time.Sleep(r.Wait)
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,514 @@
|
|||
package testutil
|
||||
|
||||
// TestServer is a test helper. It uses a fork/exec model to create
|
||||
// a test Consul server instance in the background and initialize it
|
||||
// with some data and/or services. The test server can then be used
|
||||
// to run a unit test, and offers an easy API to tear itself down
|
||||
// when the test has completed. The only prerequisite is to have a consul
|
||||
// binary available on the $PATH.
|
||||
//
|
||||
// This package does not use Consul's official API client. This is
|
||||
// because we use TestServer to test the API client, which would
|
||||
// otherwise cause an import cycle.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/sdk/freeport"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TestPerformanceConfig configures the performance parameters.
|
||||
type TestPerformanceConfig struct {
|
||||
RaftMultiplier uint `json:"raft_multiplier,omitempty"`
|
||||
}
|
||||
|
||||
// TestPortConfig configures the various ports used for services
|
||||
// provided by the Consul server.
|
||||
type TestPortConfig struct {
|
||||
DNS int `json:"dns,omitempty"`
|
||||
HTTP int `json:"http,omitempty"`
|
||||
HTTPS int `json:"https,omitempty"`
|
||||
SerfLan int `json:"serf_lan,omitempty"`
|
||||
SerfWan int `json:"serf_wan,omitempty"`
|
||||
Server int `json:"server,omitempty"`
|
||||
ProxyMinPort int `json:"proxy_min_port,omitempty"`
|
||||
ProxyMaxPort int `json:"proxy_max_port,omitempty"`
|
||||
}
|
||||
|
||||
// TestAddressConfig contains the bind addresses for various
|
||||
// components of the Consul server.
|
||||
type TestAddressConfig struct {
|
||||
HTTP string `json:"http,omitempty"`
|
||||
}
|
||||
|
||||
// TestNetworkSegment contains the configuration for a network segment.
|
||||
type TestNetworkSegment struct {
|
||||
Name string `json:"name"`
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
Advertise string `json:"advertise"`
|
||||
}
|
||||
|
||||
// TestServerConfig is the main server configuration struct.
|
||||
type TestServerConfig struct {
|
||||
NodeName string `json:"node_name"`
|
||||
NodeID string `json:"node_id"`
|
||||
NodeMeta map[string]string `json:"node_meta,omitempty"`
|
||||
Performance *TestPerformanceConfig `json:"performance,omitempty"`
|
||||
Bootstrap bool `json:"bootstrap,omitempty"`
|
||||
Server bool `json:"server,omitempty"`
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
Datacenter string `json:"datacenter,omitempty"`
|
||||
Segments []TestNetworkSegment `json:"segments"`
|
||||
DisableCheckpoint bool `json:"disable_update_check"`
|
||||
LogLevel string `json:"log_level,omitempty"`
|
||||
Bind string `json:"bind_addr,omitempty"`
|
||||
Addresses *TestAddressConfig `json:"addresses,omitempty"`
|
||||
Ports *TestPortConfig `json:"ports,omitempty"`
|
||||
RaftProtocol int `json:"raft_protocol,omitempty"`
|
||||
ACLMasterToken string `json:"acl_master_token,omitempty"`
|
||||
ACLDatacenter string `json:"acl_datacenter,omitempty"`
|
||||
PrimaryDatacenter string `json:"primary_datacenter,omitempty"`
|
||||
ACLDefaultPolicy string `json:"acl_default_policy,omitempty"`
|
||||
ACL TestACLs `json:"acl,omitempty"`
|
||||
Encrypt string `json:"encrypt,omitempty"`
|
||||
CAFile string `json:"ca_file,omitempty"`
|
||||
CertFile string `json:"cert_file,omitempty"`
|
||||
KeyFile string `json:"key_file,omitempty"`
|
||||
VerifyIncoming bool `json:"verify_incoming,omitempty"`
|
||||
VerifyIncomingRPC bool `json:"verify_incoming_rpc,omitempty"`
|
||||
VerifyIncomingHTTPS bool `json:"verify_incoming_https,omitempty"`
|
||||
VerifyOutgoing bool `json:"verify_outgoing,omitempty"`
|
||||
EnableScriptChecks bool `json:"enable_script_checks,omitempty"`
|
||||
Connect map[string]interface{} `json:"connect,omitempty"`
|
||||
EnableDebug bool `json:"enable_debug,omitempty"`
|
||||
ReadyTimeout time.Duration `json:"-"`
|
||||
Stdout io.Writer `json:"-"`
|
||||
Stderr io.Writer `json:"-"`
|
||||
Args []string `json:"-"`
|
||||
ReturnPorts func() `json:"-"`
|
||||
}
|
||||
|
||||
type TestACLs struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
TokenReplication bool `json:"enable_token_replication,omitempty"`
|
||||
PolicyTTL string `json:"policy_ttl,omitempty"`
|
||||
TokenTTL string `json:"token_ttl,omitempty"`
|
||||
DownPolicy string `json:"down_policy,omitempty"`
|
||||
DefaultPolicy string `json:"default_policy,omitempty"`
|
||||
EnableKeyListPolicy bool `json:"enable_key_list_policy,omitempty"`
|
||||
Tokens TestTokens `json:"tokens,omitempty"`
|
||||
DisabledTTL string `json:"disabled_ttl,omitempty"`
|
||||
}
|
||||
|
||||
type TestTokens struct {
|
||||
Master string `json:"master,omitempty"`
|
||||
Replication string `json:"replication,omitempty"`
|
||||
AgentMaster string `json:"agent_master,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Agent string `json:"agent,omitempty"`
|
||||
}
|
||||
|
||||
// ServerConfigCallback is a function interface which can be
|
||||
// passed to NewTestServerConfig to modify the server config.
|
||||
type ServerConfigCallback func(c *TestServerConfig)
|
||||
|
||||
// defaultServerConfig returns a new TestServerConfig struct
|
||||
// with all of the listen ports incremented by one.
|
||||
func defaultServerConfig(t CleanupT) *TestServerConfig {
|
||||
nodeID, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ports := freeport.MustTake(6)
|
||||
logBuffer := NewLogBuffer(t)
|
||||
|
||||
return &TestServerConfig{
|
||||
NodeName: "node-" + nodeID,
|
||||
NodeID: nodeID,
|
||||
DisableCheckpoint: true,
|
||||
Performance: &TestPerformanceConfig{
|
||||
RaftMultiplier: 1,
|
||||
},
|
||||
Bootstrap: true,
|
||||
Server: true,
|
||||
LogLevel: "debug",
|
||||
Bind: "127.0.0.1",
|
||||
Addresses: &TestAddressConfig{},
|
||||
Ports: &TestPortConfig{
|
||||
DNS: ports[0],
|
||||
HTTP: ports[1],
|
||||
HTTPS: ports[2],
|
||||
SerfLan: ports[3],
|
||||
SerfWan: ports[4],
|
||||
Server: ports[5],
|
||||
},
|
||||
ReadyTimeout: 10 * time.Second,
|
||||
Connect: map[string]interface{}{
|
||||
"enabled": true,
|
||||
"ca_config": map[string]interface{}{
|
||||
// const TestClusterID causes import cycle so hard code it here.
|
||||
"cluster_id": "11111111-2222-3333-4444-555555555555",
|
||||
},
|
||||
},
|
||||
ReturnPorts: func() {
|
||||
freeport.Return(ports)
|
||||
},
|
||||
Stdout: logBuffer,
|
||||
Stderr: logBuffer,
|
||||
}
|
||||
}
|
||||
|
||||
// TestService is used to serialize a service definition.
|
||||
type TestService struct {
|
||||
ID string `json:",omitempty"`
|
||||
Name string `json:",omitempty"`
|
||||
Tags []string `json:",omitempty"`
|
||||
Address string `json:",omitempty"`
|
||||
Port int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// TestCheck is used to serialize a check definition.
|
||||
type TestCheck struct {
|
||||
ID string `json:",omitempty"`
|
||||
Name string `json:",omitempty"`
|
||||
ServiceID string `json:",omitempty"`
|
||||
TTL string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// TestKVResponse is what we use to decode KV data.
|
||||
type TestKVResponse struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// TestServer is the main server wrapper struct.
|
||||
type TestServer struct {
|
||||
cmd *exec.Cmd
|
||||
Config *TestServerConfig
|
||||
|
||||
HTTPAddr string
|
||||
HTTPSAddr string
|
||||
LANAddr string
|
||||
WANAddr string
|
||||
|
||||
HTTPClient *http.Client
|
||||
|
||||
tmpdir string
|
||||
}
|
||||
|
||||
// NewTestServerConfig creates a new TestServer, and makes a call to an optional
|
||||
// callback function to modify the configuration. If there is an error
|
||||
// configuring or starting the server, the server will NOT be running when the
|
||||
// function returns (thus you do not need to stop it).
|
||||
func NewTestServerConfigT(t testing.TB, cb ServerConfigCallback) (*TestServer, error) {
|
||||
path, err := exec.LookPath("consul")
|
||||
if err != nil || path == "" {
|
||||
return nil, fmt.Errorf("consul not found on $PATH - download and install " +
|
||||
"consul or skip this test")
|
||||
}
|
||||
|
||||
prefix := "consul"
|
||||
if t != nil {
|
||||
// Use test name for tmpdir if available
|
||||
prefix = strings.Replace(t.Name(), "/", "_", -1)
|
||||
}
|
||||
tmpdir, err := ioutil.TempDir("", prefix)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create tempdir")
|
||||
}
|
||||
|
||||
cfg := defaultServerConfig(t)
|
||||
cfg.DataDir = filepath.Join(tmpdir, "data")
|
||||
if cb != nil {
|
||||
cb(cfg)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
cfg.ReturnPorts()
|
||||
os.RemoveAll(tmpdir)
|
||||
return nil, errors.Wrap(err, "failed marshaling json")
|
||||
}
|
||||
|
||||
t.Logf("CONFIG JSON: %s", string(b))
|
||||
configFile := filepath.Join(tmpdir, "config.json")
|
||||
if err := ioutil.WriteFile(configFile, b, 0644); err != nil {
|
||||
cfg.ReturnPorts()
|
||||
os.RemoveAll(tmpdir)
|
||||
return nil, errors.Wrap(err, "failed writing config content")
|
||||
}
|
||||
|
||||
// Start the server
|
||||
args := []string{"agent", "-config-file", configFile}
|
||||
args = append(args, cfg.Args...)
|
||||
cmd := exec.Command("consul", args...)
|
||||
cmd.Stdout = cfg.Stdout
|
||||
cmd.Stderr = cfg.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
cfg.ReturnPorts()
|
||||
os.RemoveAll(tmpdir)
|
||||
return nil, errors.Wrap(err, "failed starting command")
|
||||
}
|
||||
|
||||
httpAddr := fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTP)
|
||||
client := cleanhttp.DefaultClient()
|
||||
if strings.HasPrefix(cfg.Addresses.HTTP, "unix://") {
|
||||
httpAddr = cfg.Addresses.HTTP
|
||||
tr := cleanhttp.DefaultTransport()
|
||||
tr.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", httpAddr[len("unix://"):])
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
server := &TestServer{
|
||||
Config: cfg,
|
||||
cmd: cmd,
|
||||
|
||||
HTTPAddr: httpAddr,
|
||||
HTTPSAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTPS),
|
||||
LANAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfLan),
|
||||
WANAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfWan),
|
||||
|
||||
HTTPClient: client,
|
||||
|
||||
tmpdir: tmpdir,
|
||||
}
|
||||
|
||||
// Wait for the server to be ready
|
||||
if err := server.waitForAPI(); err != nil {
|
||||
if err := server.Stop(); err != nil {
|
||||
t.Logf("server stop failed with: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Stop stops the test Consul server, and removes the Consul data
|
||||
// directory once we are done.
|
||||
func (s *TestServer) Stop() error {
|
||||
defer s.Config.ReturnPorts()
|
||||
defer os.RemoveAll(s.tmpdir)
|
||||
|
||||
// There was no process
|
||||
if s.cmd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.cmd.Process != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := s.cmd.Process.Kill(); err != nil {
|
||||
return errors.Wrap(err, "failed to kill consul server")
|
||||
}
|
||||
} else { // interrupt is not supported in windows
|
||||
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
|
||||
return errors.Wrap(err, "failed to kill consul server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitDone := make(chan error)
|
||||
go func() {
|
||||
waitDone <- s.cmd.Wait()
|
||||
close(waitDone)
|
||||
}()
|
||||
|
||||
// wait for the process to exit to be sure that the data dir can be
|
||||
// deleted on all platforms.
|
||||
select {
|
||||
case err := <-waitDone:
|
||||
return err
|
||||
case <-time.After(10 * time.Second):
|
||||
s.cmd.Process.Signal(syscall.SIGABRT)
|
||||
s.cmd.Wait()
|
||||
return fmt.Errorf("timeout waiting for server to stop gracefully")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForAPI waits for the /status/leader HTTP endpoint to start
|
||||
// responding. This is an indication that the agent has started,
|
||||
// but will likely return before a leader is elected.
|
||||
// Note: We do not check for a successful response status because
|
||||
// we want this function to return without error even when
|
||||
// there's no leader elected.
|
||||
func (s *TestServer) waitForAPI() error {
|
||||
var failed bool
|
||||
|
||||
// This retry replicates the logic of retry.Run to allow for nested retries.
|
||||
// By returning an error we can wrap TestServer creation with retry.Run
|
||||
// in makeClientWithConfig.
|
||||
timer := retry.TwoSeconds()
|
||||
deadline := time.Now().Add(timer.Timeout)
|
||||
for !time.Now().After(deadline) {
|
||||
time.Sleep(timer.Wait)
|
||||
|
||||
url := s.url("/v1/status/leader")
|
||||
resp, err := s.masterGet(url)
|
||||
if err != nil {
|
||||
failed = true
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
failed = false
|
||||
}
|
||||
if failed {
|
||||
return fmt.Errorf("api unavailable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForLeader waits for the Consul server's HTTP API to become
|
||||
// available, and then waits for a known leader and an index of
|
||||
// 2 or more to be observed to confirm leader election is done.
|
||||
func (s *TestServer) WaitForLeader(t *testing.T) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
// Query the API and check the status code.
|
||||
url := s.url("/v1/catalog/nodes")
|
||||
resp, err := s.masterGet(url)
|
||||
if err != nil {
|
||||
r.Fatalf("failed http get '%s': %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
r.Fatal("failed OK response", err)
|
||||
}
|
||||
|
||||
// Ensure we have a leader and a node registration.
|
||||
if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" {
|
||||
r.Fatalf("Consul leader status: %#v", leader)
|
||||
}
|
||||
index, err := strconv.ParseInt(resp.Header.Get("X-Consul-Index"), 10, 64)
|
||||
if err != nil {
|
||||
r.Fatal("bad consul index", err)
|
||||
}
|
||||
if index < 2 {
|
||||
r.Fatal("consul index should be at least 2")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WaitForActiveCARoot waits until the server can return a Connect CA meaning
|
||||
// connect has completed bootstrapping and is ready to use.
|
||||
func (s *TestServer) WaitForActiveCARoot(t *testing.T) {
|
||||
// don't need to fully decode the response
|
||||
type rootsResponse struct {
|
||||
ActiveRootID string
|
||||
TrustDomain string
|
||||
Roots []interface{}
|
||||
}
|
||||
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
// Query the API and check the status code.
|
||||
url := s.url("/v1/agent/connect/ca/roots")
|
||||
resp, err := s.masterGet(url)
|
||||
if err != nil {
|
||||
r.Fatalf("failed http get '%s': %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Roots will return an error status until it's been bootstrapped. We could
|
||||
// parse the body and sanity check but that causes either import cycles
|
||||
// since this is used in both `api` and consul test or duplication. The 200
|
||||
// is all we really need to wait for.
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
r.Fatal("failed OK response", err)
|
||||
}
|
||||
|
||||
var roots rootsResponse
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(&roots); err != nil {
|
||||
r.Fatal(err)
|
||||
}
|
||||
|
||||
if roots.ActiveRootID == "" || len(roots.Roots) < 1 {
|
||||
r.Fatalf("/v1/agent/connect/ca/roots returned 200 but without roots: %+v", roots)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WaitForSerfCheck ensures we have a node with serfHealth check registered
|
||||
// Behavior mirrors testrpc.WaitForTestAgent but avoids the dependency cycle in api pkg
|
||||
func (s *TestServer) WaitForSerfCheck(t *testing.T) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
// Query the API and check the status code.
|
||||
url := s.url("/v1/catalog/nodes?index=0")
|
||||
resp, err := s.masterGet(url)
|
||||
if err != nil {
|
||||
r.Fatal("failed http get", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
r.Fatal("failed OK response", err)
|
||||
}
|
||||
|
||||
// Watch for the anti-entropy sync to finish.
|
||||
var payload []map[string]interface{}
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
r.Fatal(err)
|
||||
}
|
||||
if len(payload) < 1 {
|
||||
r.Fatal("No nodes")
|
||||
}
|
||||
|
||||
// Ensure the serfHealth check is registered
|
||||
url = s.url(fmt.Sprintf("/v1/health/node/%s", payload[0]["Node"]))
|
||||
resp, err = s.masterGet(url)
|
||||
if err != nil {
|
||||
r.Fatal("failed http get", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
r.Fatal("failed OK response", err)
|
||||
}
|
||||
dec = json.NewDecoder(resp.Body)
|
||||
if err = dec.Decode(&payload); err != nil {
|
||||
r.Fatal(err)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, check := range payload {
|
||||
if check["CheckID"].(string) == "serfHealth" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
r.Fatal("missing serfHealth registration")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TestServer) masterGet(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.Config.ACL.Tokens.Master != "" {
|
||||
req.Header.Set("x-consul-token", s.Config.ACL.Tokens.Master)
|
||||
}
|
||||
return s.HTTPClient.Do(req)
|
||||
}
|
256
vendor/github.com/hashicorp/consul/sdk/testutil/server_methods.go
generated
vendored
Normal file
256
vendor/github.com/hashicorp/consul/sdk/testutil/server_methods.go
generated
vendored
Normal file
|
@ -0,0 +1,256 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// copied from testutil to break circular dependency
|
||||
const (
|
||||
HealthAny = "any"
|
||||
HealthPassing = "passing"
|
||||
HealthWarning = "warning"
|
||||
HealthCritical = "critical"
|
||||
HealthMaint = "maintenance"
|
||||
)
|
||||
|
||||
// JoinLAN is used to join local datacenters together.
|
||||
func (s *TestServer) JoinLAN(t *testing.T, addr string) {
|
||||
resp := s.put(t, "/v1/agent/join/"+addr, nil)
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
// JoinWAN is used to join remote datacenters together.
|
||||
func (s *TestServer) JoinWAN(t *testing.T, addr string) {
|
||||
resp := s.put(t, "/v1/agent/join/"+addr+"?wan=1", nil)
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// SetKV sets an individual key in the K/V store.
|
||||
func (s *TestServer) SetKV(t *testing.T, key string, val []byte) {
|
||||
resp := s.put(t, "/v1/kv/"+key, bytes.NewBuffer(val))
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// SetKVString sets an individual key in the K/V store, but accepts a string
|
||||
// instead of []byte.
|
||||
func (s *TestServer) SetKVString(t *testing.T, key string, val string) {
|
||||
resp := s.put(t, "/v1/kv/"+key, bytes.NewBufferString(val))
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// GetKV retrieves a single key and returns its value
|
||||
func (s *TestServer) GetKV(t *testing.T, key string) []byte {
|
||||
resp := s.get(t, "/v1/kv/"+key)
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read body: %s", err)
|
||||
}
|
||||
|
||||
var result []*TestKVResponse
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %s", err)
|
||||
}
|
||||
if len(result) < 1 {
|
||||
t.Fatalf("key does not exist: %s", key)
|
||||
}
|
||||
|
||||
v, err := base64.StdEncoding.DecodeString(result[0].Value)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to base64 decode: %s", err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// GetKVString retrieves a value from the store, but returns as a string instead
|
||||
// of []byte.
|
||||
func (s *TestServer) GetKVString(t *testing.T, key string) string {
|
||||
return string(s.GetKV(t, key))
|
||||
}
|
||||
|
||||
// PopulateKV fills the Consul KV with data from a generic map.
|
||||
func (s *TestServer) PopulateKV(t *testing.T, data map[string][]byte) {
|
||||
for k, v := range data {
|
||||
s.SetKV(t, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ListKV returns a list of keys present in the KV store. This will list all
|
||||
// keys under the given prefix recursively and return them as a slice.
|
||||
func (s *TestServer) ListKV(t *testing.T, prefix string) []string {
|
||||
resp := s.get(t, "/v1/kv/"+prefix+"?keys")
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read body: %s", err)
|
||||
}
|
||||
|
||||
var result []string
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %s", err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddService adds a new service to the Consul instance. It also
|
||||
// automatically adds a health check with the given status, which
|
||||
// can be one of "passing", "warning", or "critical".
|
||||
func (s *TestServer) AddService(t *testing.T, name, status string, tags []string) {
|
||||
s.AddAddressableService(t, name, status, "", 0, tags) // set empty address and 0 as port for non-accessible service
|
||||
}
|
||||
|
||||
// AddAddressableService adds a new service to the Consul instance by
|
||||
// passing "address" and "port". It is helpful when you need to prepare a fakeService
|
||||
// that maybe accessed with in target source code.
|
||||
// It also automatically adds a health check with the given status, which
|
||||
// can be one of "passing", "warning", or "critical", just like `AddService` does.
|
||||
func (s *TestServer) AddAddressableService(t *testing.T, name, status, address string, port int, tags []string) {
|
||||
svc := &TestService{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
Address: address,
|
||||
Port: port,
|
||||
}
|
||||
payload, err := s.encodePayload(svc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.put(t, "/v1/agent/service/register", payload)
|
||||
|
||||
chkName := "service:" + name
|
||||
chk := &TestCheck{
|
||||
Name: chkName,
|
||||
ServiceID: name,
|
||||
TTL: "10m",
|
||||
}
|
||||
payload, err = s.encodePayload(chk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.put(t, "/v1/agent/check/register", payload)
|
||||
|
||||
switch status {
|
||||
case HealthPassing:
|
||||
s.put(t, "/v1/agent/check/pass/"+chkName, nil)
|
||||
case HealthWarning:
|
||||
s.put(t, "/v1/agent/check/warn/"+chkName, nil)
|
||||
case HealthCritical:
|
||||
s.put(t, "/v1/agent/check/fail/"+chkName, nil)
|
||||
default:
|
||||
t.Fatalf("Unrecognized status: %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
// AddCheck adds a check to the Consul instance. If the serviceID is
|
||||
// left empty (""), then the check will be associated with the node.
|
||||
// The check status may be "passing", "warning", or "critical".
|
||||
func (s *TestServer) AddCheck(t *testing.T, name, serviceID, status string) {
|
||||
chk := &TestCheck{
|
||||
ID: name,
|
||||
Name: name,
|
||||
TTL: "10m",
|
||||
}
|
||||
if serviceID != "" {
|
||||
chk.ServiceID = serviceID
|
||||
}
|
||||
|
||||
payload, err := s.encodePayload(chk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.put(t, "/v1/agent/check/register", payload)
|
||||
|
||||
switch status {
|
||||
case HealthPassing:
|
||||
s.put(t, "/v1/agent/check/pass/"+name, nil)
|
||||
case HealthWarning:
|
||||
s.put(t, "/v1/agent/check/warn/"+name, nil)
|
||||
case HealthCritical:
|
||||
s.put(t, "/v1/agent/check/fail/"+name, nil)
|
||||
default:
|
||||
t.Fatalf("Unrecognized status: %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
// put performs a new HTTP PUT request.
|
||||
func (s *TestServer) put(t *testing.T, path string, body io.Reader) *http.Response {
|
||||
req, err := http.NewRequest("PUT", s.url(path), body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create PUT request: %s", err)
|
||||
}
|
||||
resp, err := s.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make PUT request: %s", err)
|
||||
}
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
defer resp.Body.Close()
|
||||
t.Fatalf("not OK PUT: %s", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// get performs a new HTTP GET request.
|
||||
func (s *TestServer) get(t *testing.T, path string) *http.Response {
|
||||
resp, err := s.HTTPClient.Get(s.url(path))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create GET request: %s", err)
|
||||
}
|
||||
if err := s.requireOK(resp); err != nil {
|
||||
defer resp.Body.Close()
|
||||
t.Fatalf("not OK GET: %s", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// encodePayload returns a new io.Reader wrapping the encoded contents
|
||||
// of the payload, suitable for passing directly to a new request.
|
||||
func (s *TestServer) encodePayload(payload interface{}) (io.Reader, error) {
|
||||
var encoded bytes.Buffer
|
||||
enc := json.NewEncoder(&encoded)
|
||||
if err := enc.Encode(payload); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to encode payload")
|
||||
}
|
||||
return &encoded, nil
|
||||
}
|
||||
|
||||
// url is a helper function which takes a relative URL and
|
||||
// makes it into a proper URL against the local Consul server.
|
||||
func (s *TestServer) url(path string) string {
|
||||
if s == nil {
|
||||
log.Fatal("s is nil")
|
||||
}
|
||||
if s.Config == nil {
|
||||
log.Fatal("s.Config is nil")
|
||||
}
|
||||
if s.Config.Ports == nil {
|
||||
log.Fatal("s.Config.Ports is nil")
|
||||
}
|
||||
if s.Config.Ports.HTTP == 0 {
|
||||
log.Fatal("s.Config.Ports.HTTP is 0")
|
||||
}
|
||||
if path == "" {
|
||||
log.Fatal("path is empty")
|
||||
}
|
||||
return fmt.Sprintf("http://127.0.0.1:%d%s", s.Config.Ports.HTTP, path)
|
||||
}
|
||||
|
||||
// requireOK checks the HTTP response code and ensures it is acceptable.
|
||||
func (s *TestServer) requireOK(resp *http.Response) error {
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("Bad status code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package testutil
|
||||
|
||||
import "testing"
|
||||
|
||||
type WrappedServer struct {
|
||||
s *TestServer
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
// Wrap wraps the test server in a `testing.t` for convenience.
|
||||
//
|
||||
// For example, the following code snippets are equivalent.
|
||||
//
|
||||
// server.JoinLAN(t, "1.2.3.4")
|
||||
// server.Wrap(t).JoinLAN("1.2.3.4")
|
||||
//
|
||||
// This is useful when you are calling multiple functions and save the wrapped
|
||||
// value as another variable to reduce the inclusion of "t".
|
||||
func (s *TestServer) Wrap(t *testing.T) *WrappedServer {
|
||||
return &WrappedServer{s, t}
|
||||
}
|
||||
|
||||
func (w *WrappedServer) JoinLAN(addr string) {
|
||||
w.s.JoinLAN(w.t, addr)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) JoinWAN(addr string) {
|
||||
w.s.JoinWAN(w.t, addr)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) SetKV(key string, val []byte) {
|
||||
w.s.SetKV(w.t, key, val)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) SetKVString(key string, val string) {
|
||||
w.s.SetKVString(w.t, key, val)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) GetKV(key string) []byte {
|
||||
return w.s.GetKV(w.t, key)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) GetKVString(key string) string {
|
||||
return w.s.GetKVString(w.t, key)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) PopulateKV(data map[string][]byte) {
|
||||
w.s.PopulateKV(w.t, data)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) ListKV(prefix string) []string {
|
||||
return w.s.ListKV(w.t, prefix)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) AddService(name, status string, tags []string) {
|
||||
w.s.AddService(w.t, name, status, tags)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) AddAddressableService(name, status, address string, port int, tags []string) {
|
||||
w.s.AddAddressableService(w.t, name, status, address, port, tags)
|
||||
}
|
||||
|
||||
func (w *WrappedServer) AddCheck(name, serviceID, status string) {
|
||||
w.s.AddCheck(w.t, name, serviceID, status)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
func Logger(t testing.TB) hclog.InterceptLogger {
|
||||
return LoggerWithOutput(t, NewLogBuffer(t))
|
||||
}
|
||||
|
||||
func LoggerWithOutput(t testing.TB, output io.Writer) hclog.InterceptLogger {
|
||||
return hclog.NewInterceptLogger(&hclog.LoggerOptions{
|
||||
Name: t.Name(),
|
||||
Level: hclog.Trace,
|
||||
Output: output,
|
||||
})
|
||||
}
|
||||
|
||||
var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1"
|
||||
|
||||
// NewLogBuffer returns an io.Writer which buffers all writes. When the test
|
||||
// ends, t.Failed is checked. If the test has failed all log output is printed
|
||||
// to stdout.
|
||||
//
|
||||
// Set the env var NOLOGBUFFER=1 to disable buffering, resulting in all log
|
||||
// output being written immediately to stdout.
|
||||
func NewLogBuffer(t CleanupT) io.Writer {
|
||||
if sendTestLogsToStdout {
|
||||
return os.Stdout
|
||||
}
|
||||
buf := &logBuffer{buf: new(bytes.Buffer)}
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
buf.Lock()
|
||||
defer buf.Unlock()
|
||||
buf.buf.WriteTo(os.Stdout)
|
||||
}
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
type CleanupT interface {
|
||||
Cleanup(f func())
|
||||
Failed() bool
|
||||
}
|
||||
|
||||
type logBuffer struct {
|
||||
buf *bytes.Buffer
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (lb *logBuffer) Write(p []byte) (n int, err error) {
|
||||
lb.Lock()
|
||||
defer lb.Unlock()
|
||||
return lb.buf.Write(p)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
sudo: false
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.12.4
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
script: make updatedeps test
|
|
@ -25,6 +25,8 @@ fails so that the full request can be attempted again. See the
|
|||
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp) for more
|
||||
details.
|
||||
|
||||
Version 0.6.0 and before are compatible with Go prior to 1.12. From 0.6.1 onward, Go 1.12+ is required.
|
||||
|
||||
Example Use
|
||||
===========
|
||||
|
||||
|
@ -42,5 +44,18 @@ The returned response object is an `*http.Response`, the same thing you would
|
|||
usually get from `net/http`. Had the request failed one or more times, the above
|
||||
call would block and retry with exponential backoff.
|
||||
|
||||
## Getting a stdlib `*http.Client` with retries
|
||||
|
||||
It's possible to convert a `*retryablehttp.Client` directly to a `*http.Client`.
|
||||
This makes use of retryablehttp broadly applicable with minimal effort. Simply
|
||||
configure a `*retryablehttp.Client` as you wish, and then call `StandardClient()`:
|
||||
|
||||
```go
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 10
|
||||
|
||||
standardClient := retryClient.StandardClient() // *http.Client
|
||||
```
|
||||
|
||||
For more usage and examples see the
|
||||
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp).
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// The retryablehttp package provides a familiar HTTP client interface with
|
||||
// Package retryablehttp provides a familiar HTTP client interface with
|
||||
// automatic retries and exponential backoff. It is a thin wrapper over the
|
||||
// standard net/http client library and exposes nearly the same public API.
|
||||
// This makes retryablehttp very easy to drop into existing programs.
|
||||
|
@ -24,6 +24,7 @@ package retryablehttp
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -33,7 +34,10 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
|
@ -45,6 +49,9 @@ var (
|
|||
defaultRetryWaitMax = 30 * time.Second
|
||||
defaultRetryMax = 4
|
||||
|
||||
// defaultLogger is the logger provided with defaultClient
|
||||
defaultLogger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
|
||||
// defaultClient is used for performing requests without explicitly making
|
||||
// a new client. It is purposely private to avoid modifications.
|
||||
defaultClient = NewClient()
|
||||
|
@ -52,6 +59,16 @@ var (
|
|||
// We need to consume response bodies to maintain http connections, but
|
||||
// limit the size we consume to respReadLimit.
|
||||
respReadLimit = int64(4096)
|
||||
|
||||
// A regular expression to match the error returned by net/http when the
|
||||
// configured number of redirects is exhausted. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
|
||||
|
||||
// A regular expression to match the error returned by net/http when the
|
||||
// scheme specified in the URL is invalid. This error isn't typed
|
||||
// specifically so we resort to matching on the error string.
|
||||
schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
|
||||
)
|
||||
|
||||
// ReaderFunc is the type of function that can be given natively to NewRequest
|
||||
|
@ -103,95 +120,127 @@ func (r *Request) BodyBytes() ([]byte, error) {
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// SetBody allows setting the request body.
|
||||
//
|
||||
// It is useful if a new body needs to be set without constructing a new Request.
|
||||
func (r *Request) SetBody(rawBody interface{}) error {
|
||||
bodyReader, contentLength, err := getBodyReaderAndContentLength(rawBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.body = bodyReader
|
||||
r.ContentLength = contentLength
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTo allows copying the request body into a writer.
|
||||
//
|
||||
// It writes data to w until there's no more data to write or
|
||||
// when an error occurs. The return int64 value is the number of bytes
|
||||
// written. Any error encountered during the write is also returned.
|
||||
// The signature matches io.WriterTo interface.
|
||||
func (r *Request) WriteTo(w io.Writer) (int64, error) {
|
||||
body, err := r.body()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if c, ok := body.(io.Closer); ok {
|
||||
defer c.Close()
|
||||
}
|
||||
return io.Copy(w, body)
|
||||
}
|
||||
|
||||
func getBodyReaderAndContentLength(rawBody interface{}) (ReaderFunc, int64, error) {
|
||||
var bodyReader ReaderFunc
|
||||
var contentLength int64
|
||||
|
||||
if rawBody != nil {
|
||||
switch body := rawBody.(type) {
|
||||
// If they gave us a function already, great! Use it.
|
||||
case ReaderFunc:
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
case func() (io.Reader, error):
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
// If a regular byte slice, we can read it over and over via new
|
||||
// readers
|
||||
case []byte:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// If a bytes.Buffer we can read the underlying byte slice over and
|
||||
// over
|
||||
case *bytes.Buffer:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf.Bytes()), nil
|
||||
}
|
||||
contentLength = int64(buf.Len())
|
||||
|
||||
// We prioritize *bytes.Reader here because we don't really want to
|
||||
// deal with it seeking so want it to match here instead of the
|
||||
// io.ReadSeeker case.
|
||||
case *bytes.Reader:
|
||||
buf, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// Compat case
|
||||
case io.ReadSeeker:
|
||||
raw := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
_, err := raw.Seek(0, 0)
|
||||
return ioutil.NopCloser(raw), err
|
||||
}
|
||||
if lr, ok := raw.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
|
||||
// Read all in so we can reset
|
||||
case io.Reader:
|
||||
buf, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("cannot handle type %T", rawBody)
|
||||
switch body := rawBody.(type) {
|
||||
// If they gave us a function already, great! Use it.
|
||||
case ReaderFunc:
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
case func() (io.Reader, error):
|
||||
bodyReader = body
|
||||
tmp, err := body()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if lr, ok := tmp.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
if c, ok := tmp.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
// If a regular byte slice, we can read it over and over via new
|
||||
// readers
|
||||
case []byte:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// If a bytes.Buffer we can read the underlying byte slice over and
|
||||
// over
|
||||
case *bytes.Buffer:
|
||||
buf := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf.Bytes()), nil
|
||||
}
|
||||
contentLength = int64(buf.Len())
|
||||
|
||||
// We prioritize *bytes.Reader here because we don't really want to
|
||||
// deal with it seeking so want it to match here instead of the
|
||||
// io.ReadSeeker case.
|
||||
case *bytes.Reader:
|
||||
buf, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// Compat case
|
||||
case io.ReadSeeker:
|
||||
raw := body
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
_, err := raw.Seek(0, 0)
|
||||
return ioutil.NopCloser(raw), err
|
||||
}
|
||||
if lr, ok := raw.(LenReader); ok {
|
||||
contentLength = int64(lr.Len())
|
||||
}
|
||||
|
||||
// Read all in so we can reset
|
||||
case io.Reader:
|
||||
buf, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
bodyReader = func() (io.Reader, error) {
|
||||
return bytes.NewReader(buf), nil
|
||||
}
|
||||
contentLength = int64(len(buf))
|
||||
|
||||
// No body provided, nothing to do
|
||||
case nil:
|
||||
|
||||
// Unrecognized type
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("cannot handle type %T", rawBody)
|
||||
}
|
||||
return bodyReader, contentLength, nil
|
||||
}
|
||||
|
@ -228,6 +277,28 @@ type Logger interface {
|
|||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
// LeveledLogger is an interface that can be implemented by any logger or a
|
||||
// logger wrapper to provide leveled logging. The methods accept a message
|
||||
// string and a variadic number of key-value pairs. For log.Printf style
|
||||
// formatting where message string contains a format specifier, use Logger
|
||||
// interface.
|
||||
type LeveledLogger interface {
|
||||
Error(msg string, keysAndValues ...interface{})
|
||||
Info(msg string, keysAndValues ...interface{})
|
||||
Debug(msg string, keysAndValues ...interface{})
|
||||
Warn(msg string, keysAndValues ...interface{})
|
||||
}
|
||||
|
||||
// hookLogger adapts an LeveledLogger to Logger for use by the existing hook functions
|
||||
// without changing the API.
|
||||
type hookLogger struct {
|
||||
LeveledLogger
|
||||
}
|
||||
|
||||
func (h hookLogger) Printf(s string, args ...interface{}) {
|
||||
h.Info(fmt.Sprintf(s, args...))
|
||||
}
|
||||
|
||||
// RequestLogHook allows a function to run before each retry. The HTTP
|
||||
// request which will be made, and the retry number (0 for the initial
|
||||
// request) are available to users. The internal logger is exposed to
|
||||
|
@ -247,7 +318,7 @@ type ResponseLogHook func(Logger, *http.Response)
|
|||
// and returns the response to the caller. If CheckRetry returns an error,
|
||||
// that error value is returned in lieu of the error from the request. The
|
||||
// Client will close any response body when retrying, but if the retry is
|
||||
// aborted it is up to the CheckResponse callback to properly close any
|
||||
// aborted it is up to the CheckRetry callback to properly close any
|
||||
// response body before returning.
|
||||
type CheckRetry func(ctx context.Context, resp *http.Response, err error) (bool, error)
|
||||
|
||||
|
@ -266,7 +337,7 @@ type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Resp
|
|||
// like automatic retries to tolerate minor outages.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client // Internal HTTP client.
|
||||
Logger Logger // Customer logger instance.
|
||||
Logger interface{} // Customer logger instance. Can be either Logger or LeveledLogger
|
||||
|
||||
RetryWaitMin time.Duration // Minimum time to wait
|
||||
RetryWaitMax time.Duration // Maximum time to wait
|
||||
|
@ -289,13 +360,16 @@ type Client struct {
|
|||
|
||||
// ErrorHandler specifies the custom error handler to use, if any
|
||||
ErrorHandler ErrorHandler
|
||||
|
||||
loggerInit sync.Once
|
||||
clientInit sync.Once
|
||||
}
|
||||
|
||||
// NewClient creates a new Client with default settings.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
HTTPClient: cleanhttp.DefaultClient(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
HTTPClient: cleanhttp.DefaultPooledClient(),
|
||||
Logger: defaultLogger,
|
||||
RetryWaitMin: defaultRetryWaitMin,
|
||||
RetryWaitMax: defaultRetryWaitMax,
|
||||
RetryMax: defaultRetryMax,
|
||||
|
@ -304,6 +378,24 @@ func NewClient() *Client {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Client) logger() interface{} {
|
||||
c.loggerInit.Do(func() {
|
||||
if c.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch c.Logger.(type) {
|
||||
case Logger, LeveledLogger:
|
||||
// ok
|
||||
default:
|
||||
// This should happen in dev when they are setting Logger and work on code, not in prod.
|
||||
panic(fmt.Sprintf("invalid logger type passed, must be Logger or LeveledLogger, was %T", c.Logger))
|
||||
}
|
||||
})
|
||||
|
||||
return c.Logger
|
||||
}
|
||||
|
||||
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
||||
// will retry on connection errors and server errors.
|
||||
func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
|
@ -313,8 +405,34 @@ func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bo
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
if v, ok := err.(*url.Error); ok {
|
||||
// Don't retry if the error was due to too many redirects.
|
||||
if redirectsErrorRe.MatchString(v.Error()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to an invalid protocol scheme.
|
||||
if schemeErrorRe.MatchString(v.Error()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to TLS cert verification failure.
|
||||
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// The error is likely recoverable so retry.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 429 Too Many Requests is recoverable. Sometimes the server puts
|
||||
// a Retry-After response header to indicate when the server is
|
||||
// available to start processing request from client.
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check the response code. We retry on 500-range responses to allow
|
||||
// the server time to recover, as 500's are typically not permanent
|
||||
// errors and may relate to outages on the server side. This will catch
|
||||
|
@ -326,10 +444,66 @@ func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bo
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// ErrorPropagatedRetryPolicy is the same as DefaultRetryPolicy, except it
|
||||
// propagates errors back instead of returning nil. This allows you to inspect
|
||||
// why it decided to retry or not.
|
||||
func ErrorPropagatedRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
// do not retry on context.Canceled or context.DeadlineExceeded
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if v, ok := err.(*url.Error); ok {
|
||||
// Don't retry if the error was due to too many redirects.
|
||||
if redirectsErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to an invalid protocol scheme.
|
||||
if schemeErrorRe.MatchString(v.Error()) {
|
||||
return false, v
|
||||
}
|
||||
|
||||
// Don't retry if the error was due to TLS cert verification failure.
|
||||
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
|
||||
return false, v
|
||||
}
|
||||
}
|
||||
|
||||
// The error is likely recoverable so retry.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check the response code. We retry on 500-range responses to allow
|
||||
// the server time to recover, as 500's are typically not permanent
|
||||
// errors and may relate to outages on the server side. This will catch
|
||||
// invalid response codes as well, like 0 and 999.
|
||||
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != 501) {
|
||||
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DefaultBackoff provides a default callback for Client.Backoff which
|
||||
// will perform exponential backoff based on the attempt number and limited
|
||||
// by the provided minimum and maximum durations.
|
||||
//
|
||||
// It also tries to parse Retry-After response header when a http.StatusTooManyRequests
|
||||
// (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
|
||||
// seconds the server states it may be ready to process more requests from this client.
|
||||
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
if s, ok := resp.Header["Retry-After"]; ok {
|
||||
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
|
||||
return time.Second * time.Duration(sleep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mult := math.Pow(2, float64(attemptNum)) * float64(min)
|
||||
sleep := time.Duration(mult)
|
||||
if float64(sleep) != mult || sleep > max {
|
||||
|
@ -342,7 +516,7 @@ func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response)
|
|||
// perform linear backoff based on the attempt number and with jitter to
|
||||
// prevent a thundering herd.
|
||||
//
|
||||
// min and max here are *not* absolute values. The number to be multipled by
|
||||
// min and max here are *not* absolute values. The number to be multiplied by
|
||||
// the attempt number will be chosen at random from between them, thus they are
|
||||
// bounding the jitter.
|
||||
//
|
||||
|
@ -385,20 +559,38 @@ func PassthroughErrorHandler(resp *http.Response, err error, _ int) (*http.Respo
|
|||
|
||||
// Do wraps calling an HTTP method with retries.
|
||||
func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||
if c.Logger != nil {
|
||||
c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL)
|
||||
c.clientInit.Do(func() {
|
||||
if c.HTTPClient == nil {
|
||||
c.HTTPClient = cleanhttp.DefaultPooledClient()
|
||||
}
|
||||
})
|
||||
|
||||
logger := c.logger()
|
||||
|
||||
if logger != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Debug("performing request", "method", req.Method, "url", req.URL)
|
||||
case Logger:
|
||||
v.Printf("[DEBUG] %s %s", req.Method, req.URL)
|
||||
}
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
var attempt int
|
||||
var shouldRetry bool
|
||||
var doErr, checkErr error
|
||||
|
||||
for i := 0; ; i++ {
|
||||
attempt++
|
||||
|
||||
var code int // HTTP response code
|
||||
|
||||
// Always rewind the request body when non-nil.
|
||||
if req.body != nil {
|
||||
body, err := req.body()
|
||||
if err != nil {
|
||||
c.HTTPClient.CloseIdleConnections()
|
||||
return resp, err
|
||||
}
|
||||
if c, ok := body.(io.ReadCloser); ok {
|
||||
|
@ -409,40 +601,53 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
|||
}
|
||||
|
||||
if c.RequestLogHook != nil {
|
||||
c.RequestLogHook(c.Logger, req.Request, i)
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
c.RequestLogHook(hookLogger{v}, req.Request, i)
|
||||
case Logger:
|
||||
c.RequestLogHook(v, req.Request, i)
|
||||
default:
|
||||
c.RequestLogHook(nil, req.Request, i)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt the request
|
||||
resp, err = c.HTTPClient.Do(req.Request)
|
||||
resp, doErr = c.HTTPClient.Do(req.Request)
|
||||
if resp != nil {
|
||||
code = resp.StatusCode
|
||||
}
|
||||
|
||||
// Check if we should continue with retries.
|
||||
checkOK, checkErr := c.CheckRetry(req.Context(), resp, err)
|
||||
shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, doErr)
|
||||
|
||||
if err != nil {
|
||||
if c.Logger != nil {
|
||||
c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
|
||||
if doErr != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Error("request failed", "error", doErr, "method", req.Method, "url", req.URL)
|
||||
case Logger:
|
||||
v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, doErr)
|
||||
}
|
||||
} else {
|
||||
// Call this here to maintain the behavior of logging all requests,
|
||||
// even if CheckRetry signals to stop.
|
||||
if c.ResponseLogHook != nil {
|
||||
// Call the response logger function if provided.
|
||||
c.ResponseLogHook(c.Logger, resp)
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
c.ResponseLogHook(hookLogger{v}, resp)
|
||||
case Logger:
|
||||
c.ResponseLogHook(v, resp)
|
||||
default:
|
||||
c.ResponseLogHook(nil, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now decide if we should continue.
|
||||
if !checkOK {
|
||||
if checkErr != nil {
|
||||
err = checkErr
|
||||
}
|
||||
return resp, err
|
||||
if !shouldRetry {
|
||||
break
|
||||
}
|
||||
|
||||
// We do this before drainBody beause there's no need for the I/O if
|
||||
// We do this before drainBody because there's no need for the I/O if
|
||||
// we're breaking out
|
||||
remain := c.RetryMax - i
|
||||
if remain <= 0 {
|
||||
|
@ -450,7 +655,7 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
|||
}
|
||||
|
||||
// We're going to retry, consume any response to reuse the connection.
|
||||
if err == nil && resp != nil {
|
||||
if doErr == nil {
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
|
||||
|
@ -459,27 +664,58 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
|||
if code > 0 {
|
||||
desc = fmt.Sprintf("%s (status: %d)", desc, code)
|
||||
}
|
||||
if c.Logger != nil {
|
||||
c.Logger.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
||||
if logger != nil {
|
||||
switch v := logger.(type) {
|
||||
case LeveledLogger:
|
||||
v.Debug("retrying request", "request", desc, "timeout", wait, "remaining", remain)
|
||||
case Logger:
|
||||
v.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
c.HTTPClient.CloseIdleConnections()
|
||||
return nil, req.Context().Err()
|
||||
case <-time.After(wait):
|
||||
}
|
||||
|
||||
// Make shallow copy of http Request so that we can modify its body
|
||||
// without racing against the closeBody call in persistConn.writeLoop.
|
||||
httpreq := *req.Request
|
||||
req.Request = &httpreq
|
||||
}
|
||||
|
||||
// this is the closest we have to success criteria
|
||||
if doErr == nil && checkErr == nil && !shouldRetry {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
defer c.HTTPClient.CloseIdleConnections()
|
||||
|
||||
err := doErr
|
||||
if checkErr != nil {
|
||||
err = checkErr
|
||||
}
|
||||
|
||||
if c.ErrorHandler != nil {
|
||||
return c.ErrorHandler(resp, err, c.RetryMax+1)
|
||||
return c.ErrorHandler(resp, err, attempt)
|
||||
}
|
||||
|
||||
// By default, we close the response body and return an error without
|
||||
// returning the response
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempts",
|
||||
req.Method, req.URL, c.RetryMax+1)
|
||||
|
||||
// this means CheckRetry thought the request was a failure, but didn't
|
||||
// communicate why
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempt(s)",
|
||||
req.Method, req.URL, attempt)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempt(s): %w",
|
||||
req.Method, req.URL, attempt, err)
|
||||
}
|
||||
|
||||
// Try to read the response body so we can reuse this connection.
|
||||
|
@ -487,8 +723,13 @@ func (c *Client) drainBody(body io.ReadCloser) {
|
|||
defer body.Close()
|
||||
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
|
||||
if err != nil {
|
||||
if c.Logger != nil {
|
||||
c.Logger.Printf("[ERR] error reading response body: %v", err)
|
||||
if c.logger() != nil {
|
||||
switch v := c.logger().(type) {
|
||||
case LeveledLogger:
|
||||
v.Error("error reading response body", "error", err)
|
||||
case Logger:
|
||||
v.Printf("[ERR] error reading response body: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -547,3 +788,11 @@ func PostForm(url string, data url.Values) (*http.Response, error) {
|
|||
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
|
||||
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
||||
|
||||
// StandardClient returns a stdlib *http.Client with a custom Transport, which
|
||||
// shims in a *retryablehttp.Client for added retries.
|
||||
func (c *Client) StandardClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &RoundTripper{Client: c},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
module github.com/hashicorp/go-retryablehttp
|
||||
|
||||
require github.com/hashicorp/go-cleanhttp v0.5.0
|
||||
require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1
|
||||
github.com/hashicorp/go-hclog v0.9.2
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
|
@ -1,2 +1,10 @@
|
|||
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package retryablehttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RoundTripper implements the http.RoundTripper interface, using a retrying
|
||||
// HTTP client to execute requests.
|
||||
//
|
||||
// It is important to note that retryablehttp doesn't always act exactly as a
|
||||
// RoundTripper should. This is highly dependent on the retryable client's
|
||||
// configuration.
|
||||
type RoundTripper struct {
|
||||
// The client to use during requests. If nil, the default retryablehttp
|
||||
// client and settings will be used.
|
||||
Client *Client
|
||||
|
||||
// once ensures that the logic to initialize the default client runs at
|
||||
// most once, in a single thread.
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// init initializes the underlying retryable client.
|
||||
func (rt *RoundTripper) init() {
|
||||
if rt.Client == nil {
|
||||
rt.Client = NewClient()
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip satisfies the http.RoundTripper interface.
|
||||
func (rt *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt.once.Do(rt.init)
|
||||
|
||||
// Convert the request to be retryable.
|
||||
retryableReq, err := FromRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Execute the request.
|
||||
resp, err := rt.Client.Do(retryableReq)
|
||||
// If we got an error returned by standard library's `Do` method, unwrap it
|
||||
// otherwise we will wind up erroneously re-nesting the error.
|
||||
if _, ok := err.(*url.Error); ok {
|
||||
return resp, errors.Unwrap(err)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
|
@ -115,6 +115,26 @@ func (c *TokenAuth) LookupSelf() (*Secret, error) {
|
|||
return ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
func (c *TokenAuth) RenewAccessor(accessor string, increment int) (*Secret, error) {
|
||||
r := c.c.NewRequest("POST", "/v1/auth/token/renew-accessor")
|
||||
if err := r.SetJSONBody(map[string]interface{}{
|
||||
"accessor": accessor,
|
||||
"increment": increment,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
resp, err := c.c.RawRequestWithContext(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
func (c *TokenAuth) Renew(token string, increment int) (*Secret, error) {
|
||||
r := c.c.NewRequest("PUT", "/v1/auth/token/renew")
|
||||
if err := r.SetJSONBody(map[string]interface{}{
|
||||
|
|
|
@ -32,6 +32,7 @@ const EnvVaultCAPath = "VAULT_CAPATH"
|
|||
const EnvVaultClientCert = "VAULT_CLIENT_CERT"
|
||||
const EnvVaultClientKey = "VAULT_CLIENT_KEY"
|
||||
const EnvVaultClientTimeout = "VAULT_CLIENT_TIMEOUT"
|
||||
const EnvVaultSRVLookup = "VAULT_SRV_LOOKUP"
|
||||
const EnvVaultSkipVerify = "VAULT_SKIP_VERIFY"
|
||||
const EnvVaultNamespace = "VAULT_NAMESPACE"
|
||||
const EnvVaultTLSServerName = "VAULT_TLS_SERVER_NAME"
|
||||
|
@ -49,7 +50,8 @@ const EnvVaultInsecure = "VAULT_SKIP_VERIFY"
|
|||
// returns an optional string duration to be used for response wrapping (e.g.
|
||||
// "15s", or simply "15"). The path will not begin with "/v1/" or "v1/" or "/",
|
||||
// however, end-of-path forward slashes are not trimmed, so must match your
|
||||
// called path precisely.
|
||||
// called path precisely. Response wrapping will only be used when the return
|
||||
// value is not the empty string.
|
||||
type WrappingLookupFunc func(operation, path string) string
|
||||
|
||||
// Config is used to configure the creation of the client.
|
||||
|
@ -88,6 +90,9 @@ type Config struct {
|
|||
// The Backoff function to use; a default is used if not provided
|
||||
Backoff retryablehttp.Backoff
|
||||
|
||||
// The CheckRetry function to use; a default is used if not provided
|
||||
CheckRetry retryablehttp.CheckRetry
|
||||
|
||||
// Limiter is the rate limiter used by the client.
|
||||
// If this pointer is nil, then there will be no limit set.
|
||||
// In contrast, if this pointer is set, even to an empty struct,
|
||||
|
@ -102,6 +107,9 @@ type Config struct {
|
|||
// Note: It is not thread-safe to set this and make concurrent requests
|
||||
// with the same client. Cloning a client will not clone this value.
|
||||
OutputCurlString bool
|
||||
|
||||
// SRVLookup enables the client to lookup the host through DNS SRV lookup
|
||||
SRVLookup bool
|
||||
}
|
||||
|
||||
// TLSConfig contains the parameters needed to configure TLS on the HTTP client
|
||||
|
@ -140,8 +148,8 @@ func DefaultConfig() *Config {
|
|||
config := &Config{
|
||||
Address: "https://127.0.0.1:8200",
|
||||
HttpClient: cleanhttp.DefaultPooledClient(),
|
||||
Timeout: time.Second * 60,
|
||||
}
|
||||
config.HttpClient.Timeout = time.Second * 60
|
||||
|
||||
transport := config.HttpClient.Transport.(*http.Transport)
|
||||
transport.TLSHandshakeTimeout = 10 * time.Second
|
||||
|
@ -242,6 +250,7 @@ func (c *Config) ReadEnvironment() error {
|
|||
var envInsecure bool
|
||||
var envTLSServerName string
|
||||
var envMaxRetries *uint64
|
||||
var envSRVLookup bool
|
||||
var limit *rate.Limiter
|
||||
|
||||
// Parse the environment variables
|
||||
|
@ -299,6 +308,13 @@ func (c *Config) ReadEnvironment() error {
|
|||
return fmt.Errorf("could not parse VAULT_INSECURE")
|
||||
}
|
||||
}
|
||||
if v := os.Getenv(EnvVaultSRVLookup); v != "" {
|
||||
var err error
|
||||
envSRVLookup, err = strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse %s", EnvVaultSRVLookup)
|
||||
}
|
||||
}
|
||||
|
||||
if v := os.Getenv(EnvVaultTLSServerName); v != "" {
|
||||
envTLSServerName = v
|
||||
|
@ -317,6 +333,7 @@ func (c *Config) ReadEnvironment() error {
|
|||
c.modifyLock.Lock()
|
||||
defer c.modifyLock.Unlock()
|
||||
|
||||
c.SRVLookup = envSRVLookup
|
||||
c.Limiter = limit
|
||||
|
||||
if err := c.ConfigureTLS(t); err != nil {
|
||||
|
@ -427,10 +444,14 @@ func NewClient(c *Config) (*Client, error) {
|
|||
}
|
||||
|
||||
client := &Client{
|
||||
addr: u,
|
||||
config: c,
|
||||
addr: u,
|
||||
config: c,
|
||||
headers: make(http.Header),
|
||||
}
|
||||
|
||||
// Add the VaultRequest SSRF protection header
|
||||
client.headers[consts.RequestHeaderName] = []string{"true"}
|
||||
|
||||
if token := os.Getenv(EnvVaultToken); token != "" {
|
||||
client.token = token
|
||||
}
|
||||
|
@ -488,6 +509,16 @@ func (c *Client) SetMaxRetries(retries int) {
|
|||
c.config.MaxRetries = retries
|
||||
}
|
||||
|
||||
// SetCheckRetry sets the CheckRetry function to be used for future requests.
|
||||
func (c *Client) SetCheckRetry(checkRetry retryablehttp.CheckRetry) {
|
||||
c.modifyLock.RLock()
|
||||
c.config.modifyLock.Lock()
|
||||
defer c.config.modifyLock.Unlock()
|
||||
c.modifyLock.RUnlock()
|
||||
|
||||
c.config.CheckRetry = checkRetry
|
||||
}
|
||||
|
||||
// SetClientTimeout sets the client request timeout
|
||||
func (c *Client) SetClientTimeout(timeout time.Duration) {
|
||||
c.modifyLock.RLock()
|
||||
|
@ -517,7 +548,7 @@ func (c *Client) SetOutputCurlString(curl bool) {
|
|||
}
|
||||
|
||||
// CurrentWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
|
||||
// for a given operation and path
|
||||
// for a given operation and path.
|
||||
func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc {
|
||||
c.modifyLock.RLock()
|
||||
defer c.modifyLock.RUnlock()
|
||||
|
@ -526,7 +557,7 @@ func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc {
|
|||
}
|
||||
|
||||
// SetWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
|
||||
// for a given operation and path
|
||||
// for a given operation and path.
|
||||
func (c *Client) SetWrappingLookupFunc(lookupFunc WrappingLookupFunc) {
|
||||
c.modifyLock.Lock()
|
||||
defer c.modifyLock.Unlock()
|
||||
|
@ -586,7 +617,7 @@ func (c *Client) ClearToken() {
|
|||
}
|
||||
|
||||
// Headers gets the current set of headers used for requests. This returns a
|
||||
// copy; to modify it make modifications locally and use SetHeaders.
|
||||
// copy; to modify it call AddHeader or SetHeaders.
|
||||
func (c *Client) Headers() http.Header {
|
||||
c.modifyLock.RLock()
|
||||
defer c.modifyLock.RUnlock()
|
||||
|
@ -605,11 +636,19 @@ func (c *Client) Headers() http.Header {
|
|||
return ret
|
||||
}
|
||||
|
||||
// SetHeaders sets the headers to be used for future requests.
|
||||
// AddHeader allows a single header key/value pair to be added
|
||||
// in a race-safe fashion.
|
||||
func (c *Client) AddHeader(key, value string) {
|
||||
c.modifyLock.Lock()
|
||||
defer c.modifyLock.Unlock()
|
||||
c.headers.Add(key, value)
|
||||
}
|
||||
|
||||
// SetHeaders clears all previous headers and uses only the given
|
||||
// ones going forward.
|
||||
func (c *Client) SetHeaders(headers http.Header) {
|
||||
c.modifyLock.Lock()
|
||||
defer c.modifyLock.Unlock()
|
||||
|
||||
c.headers = headers
|
||||
}
|
||||
|
||||
|
@ -643,6 +682,7 @@ func (c *Client) Clone() (*Client, error) {
|
|||
MaxRetries: config.MaxRetries,
|
||||
Timeout: config.Timeout,
|
||||
Backoff: config.Backoff,
|
||||
CheckRetry: config.CheckRetry,
|
||||
Limiter: config.Limiter,
|
||||
}
|
||||
config.modifyLock.RUnlock()
|
||||
|
@ -669,15 +709,14 @@ func (c *Client) NewRequest(method, requestPath string) *Request {
|
|||
token := c.token
|
||||
mfaCreds := c.mfaCreds
|
||||
wrappingLookupFunc := c.wrappingLookupFunc
|
||||
headers := c.headers
|
||||
policyOverride := c.policyOverride
|
||||
c.modifyLock.RUnlock()
|
||||
|
||||
var host = addr.Host
|
||||
// if SRV records exist (see https://tools.ietf.org/html/draft-andrews-http-srv-02), lookup the SRV
|
||||
// record and take the highest match; this is not designed for high-availability, just discovery
|
||||
var host string = addr.Host
|
||||
if addr.Port() == "" {
|
||||
// Internet Draft specifies that the SRV record is ignored if a port is given
|
||||
// Internet Draft specifies that the SRV record is ignored if a port is given
|
||||
if addr.Port() == "" && c.config.SRVLookup {
|
||||
_, addrs, err := net.LookupSRV("http", "tcp", addr.Hostname())
|
||||
if err == nil && len(addrs) > 0 {
|
||||
host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port)
|
||||
|
@ -692,6 +731,7 @@ func (c *Client) NewRequest(method, requestPath string) *Request {
|
|||
Host: host,
|
||||
Path: path.Join(addr.Path, requestPath),
|
||||
},
|
||||
Host: addr.Host,
|
||||
ClientToken: token,
|
||||
Params: make(map[string][]string),
|
||||
}
|
||||
|
@ -714,10 +754,7 @@ func (c *Client) NewRequest(method, requestPath string) *Request {
|
|||
req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath)
|
||||
}
|
||||
|
||||
if headers != nil {
|
||||
req.Headers = headers
|
||||
}
|
||||
|
||||
req.Headers = c.Headers()
|
||||
req.PolicyOverride = policyOverride
|
||||
|
||||
return req
|
||||
|
@ -740,6 +777,7 @@ func (c *Client) RawRequestWithContext(ctx context.Context, r *Request) (*Respon
|
|||
c.config.modifyLock.RLock()
|
||||
limiter := c.config.Limiter
|
||||
maxRetries := c.config.MaxRetries
|
||||
checkRetry := c.config.CheckRetry
|
||||
backoff := c.config.Backoff
|
||||
httpClient := c.config.HttpClient
|
||||
timeout := c.config.Timeout
|
||||
|
@ -776,6 +814,10 @@ START:
|
|||
}
|
||||
|
||||
if timeout != 0 {
|
||||
// Note: we purposefully do not call cancel manually. The reason is
|
||||
// when canceled, the request.Body will EOF when reading due to the way
|
||||
// it streams data in. Cancel will still be run when the timeout is
|
||||
// hit, so this doesn't really harm anything.
|
||||
ctx, _ = context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
req.Request = req.Request.WithContext(ctx)
|
||||
|
@ -784,13 +826,17 @@ START:
|
|||
backoff = retryablehttp.LinearJitterBackoff
|
||||
}
|
||||
|
||||
if checkRetry == nil {
|
||||
checkRetry = retryablehttp.DefaultRetryPolicy
|
||||
}
|
||||
|
||||
client := &retryablehttp.Client{
|
||||
HTTPClient: httpClient,
|
||||
RetryWaitMin: 1000 * time.Millisecond,
|
||||
RetryWaitMax: 1500 * time.Millisecond,
|
||||
RetryMax: maxRetries,
|
||||
CheckRetry: retryablehttp.DefaultRetryPolicy,
|
||||
Backoff: backoff,
|
||||
CheckRetry: checkRetry,
|
||||
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
module github.com/hashicorp/vault/api
|
||||
|
||||
go 1.12
|
||||
go 1.13
|
||||
|
||||
replace github.com/hashicorp/vault/sdk => ../sdk
|
||||
|
||||
require (
|
||||
github.com/go-test/deep v1.0.2
|
||||
github.com/hashicorp/errwrap v1.0.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1
|
||||
github.com/hashicorp/go-multierror v1.0.0
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4
|
||||
github.com/hashicorp/go-rootcerts v1.0.1
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6
|
||||
github.com/hashicorp/go-rootcerts v1.0.2
|
||||
github.com/hashicorp/hcl v1.0.0
|
||||
github.com/hashicorp/vault/sdk v0.1.13
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||
gopkg.in/square/go-jose.v2 v2.3.1
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267
|
||||
github.com/mitchellh/mapstructure v1.3.2
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
)
|
||||
|
|
|
@ -1,118 +1,711 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
|
||||
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
|
||||
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/Azure/azure-sdk-for-go v36.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.6.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.7.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.4.0/go.mod h1:Oo5cRhLvZteXzI2itUm5ziqsoIxRkzrt3t61FeZaS18=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.3.0/go.mod h1:rNYMNAefZMRowqCV0cVhr/YDW5dD7afFq9nXAXL4ykE=
|
||||
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/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/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
||||
github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.4.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Microsoft/go-winio v0.4.13/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo=
|
||||
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a/go.mod h1:D73UAuEPckrDorYZdtlCu2ySOLuPB5W4rhIkmmc/XbI=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190412020505-60e2075261b6/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2/go.mod h1:OMVSB21p9+xQUIqlGizHPZfjK+SHws1ht+ZytVDoz9U=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
|
||||
github.com/armon/go-metrics v0.3.1/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-proxyproto v0.0.0-20190211145416-68259f75880e/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f/go.mod h1:HQhVmdUf7dBNwIIdBTivnCDxcf6IZY3/zrb+uKSJz6Y=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/centrify/cloud-golang-sdk v0.0.0-20190214225812-119110094d0f/go.mod h1:C0rtzmGXgN78pYR0tGJFhtHgkbAs0lIbHwkB81VxDQE=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0/go.mod h1:5d8DqS60xkj9k3aXfL3+mXBH0DPYO0FQjcKosxl+b/Q=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190412130859-3b1d194e553a/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/frankban/quicktest v1.4.0/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
|
||||
github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
|
||||
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
||||
github.com/gammazero/deque v0.0.0-20190130191400-2afb3858e9c7/go.mod h1:GeIq9qoE43YdGnDXURnmKTnGg15pQz4mYkXSTChbneI=
|
||||
github.com/gammazero/workerpool v0.0.0-20190406235159-88d534f22b56/go.mod h1:w9RqFVO2BM3xwWEcAB8Fwp0OviTBBEiRmSBDfbXnd3w=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
||||
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||
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-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 h1:28FVBuwkwowZMjbA7M0wXsI6t3PYulRTMio3SO+eKCM=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/gocql/gocql v0.0.0-20190402132108-0e1d5de854df/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-metrics-stackdriver v0.2.0/go.mod h1:KLcPyp3dWJAFD+yHisGlJSZktIsTjb50eB72U2YZ9K0=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/consul-template v0.25.0/go.mod h1:/vUsrJvDuuQHcxEw0zik+YXTS7ZKWZjQeaQhshBmfH0=
|
||||
github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-gatedio v0.5.0/go.mod h1:Lr3t8L6IyxD3DAeaUxGcgl2JnRUpWMCsmBl4Omu/2t4=
|
||||
github.com/hashicorp/go-gcp-common v0.5.0/go.mod h1:IDGUI2N/OS3PiU4qZcXJeWKPI6O/9Y8hOrbSiMcqyYw=
|
||||
github.com/hashicorp/go-gcp-common v0.6.0/go.mod h1:RuZi18562/z30wxOzpjeRrGcmk9Ro/rBzixaSZDhIhY=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.10.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-kms-wrapping v0.5.1/go.mod h1:cGIibZmMx9qlxS1pZTUrEgGqA+7u3zJyvVYMhjU2bDs=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
|
||||
github.com/hashicorp/go-memdb v1.0.2/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE=
|
||||
github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a/go.mod h1:xbXnmKqX9/+RhPkJ4zrEx4738HacP72aaUPlT2RZ4sU=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.2 h1:bHM2aVXwBtBJWxHtkSrWuI4umABCUczs52eiUS9nSiw=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2-0.20191001231223-f32f5fe8d6a8/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20191220223628-edc62acd919d/go.mod h1:WKCL+tLVhN1D+APwH3JiTRZoxcdwRk86bWu1LVCUPaE=
|
||||
github.com/hashicorp/raft v1.0.1/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI=
|
||||
github.com/hashicorp/raft v1.1.2-0.20191002163536-9c6bd3e3eb17/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
|
||||
github.com/hashicorp/raft-snapshot v1.0.2-0.20190827162939-8117efcc5aab/go.mod h1:5sL9eUn72lH5DzsFIJ9jaysITbHksSSszImWSOTC8Ic=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
|
||||
github.com/hashicorp/vault v1.4.2/go.mod h1:500fLOj7p92Ys4X265LizqF78MzmHJUf1jV1zNJt060=
|
||||
github.com/hashicorp/vault-plugin-auth-alicloud v0.5.5/go.mod h1:sQ+VNwPQlemgXHXikYH6onfH9gPwDZ1GUVRLz0ZvHx8=
|
||||
github.com/hashicorp/vault-plugin-auth-azure v0.5.6-0.20200422235613-1b5c70f9ef68/go.mod h1:RCVBsf8AJndh4c6iGZtvVZFui9SG0Bj9fnF0SodNIkw=
|
||||
github.com/hashicorp/vault-plugin-auth-centrify v0.5.5/go.mod h1:GfRoy7NHsuR/ogmZtbExdJXUwbfwcxPrS9xzkyy2J/c=
|
||||
github.com/hashicorp/vault-plugin-auth-cf v0.5.4/go.mod h1:idkFYHc6ske2BE7fe00SpH+SBIlqDKz8vk/IPLJuX2o=
|
||||
github.com/hashicorp/vault-plugin-auth-gcp v0.5.1/go.mod h1:eLj92eX8MPI4vY1jaazVLF2sVbSAJ3LRHLRhF/pUmlI=
|
||||
github.com/hashicorp/vault-plugin-auth-gcp v0.6.2-0.20200428223335-82bd3a3ad5b3/go.mod h1:U0fkAlxWTEyQ74lx8wlGdD493lP1DD/qpMjXgOEbwj0=
|
||||
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2/go.mod h1:SFadxIfoLGzugEjwUUmUaCGbsYEz2/jJymZDDQjEqYg=
|
||||
github.com/hashicorp/vault-plugin-auth-kerberos v0.1.5/go.mod h1:r4UqWITHYKmBeAMKPWqLo4V8bl/wNqoSIaQcMpeK9ss=
|
||||
github.com/hashicorp/vault-plugin-auth-kubernetes v0.6.1/go.mod h1:/Y9W5aZULfPeNVRQK0/nrFGpHWyNm0J3UWhOdsAu0vM=
|
||||
github.com/hashicorp/vault-plugin-auth-oci v0.5.4/go.mod h1:j05O2b9fw2Q82NxDPhHMYVfHKvitUYGWfmqmpBdqmmc=
|
||||
github.com/hashicorp/vault-plugin-database-elasticsearch v0.5.4/go.mod h1:QjGrrxcRXv/4XkEZAlM0VMZEa3uxKAICFqDj27FP/48=
|
||||
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.2-0.20200520204052-f840e9d4895c/go.mod h1:MP3kfr0N+7miOTZFwKv952b9VkXM4S2Q6YtQCiNKWq8=
|
||||
github.com/hashicorp/vault-plugin-secrets-ad v0.6.6-0.20200520202259-fc6b89630f9f/go.mod h1:kk98nB+cwDbt3I7UGQq3ota7+eHZrGSTQZfSRGpluvA=
|
||||
github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.5/go.mod h1:gAoReoUpBHaBwkxQqTK7FY8nQC0MuaZHLiW5WOSny5g=
|
||||
github.com/hashicorp/vault-plugin-secrets-azure v0.5.6/go.mod h1:Q0cIL4kZWnMmQWkBfWtyOd7+JXTEpAyU4L932PMHq3E=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.6.2-0.20200507171538-2548e2b5058d/go.mod h1:jVTE1fuhRcBOb/gnCT9W++AnlwiyQEX4S8iVCKhKQsE=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.5.5/go.mod h1:b6RwFD1bny1zbfqhD35iGJdQYHRtJLx3HfBD109GO38=
|
||||
github.com/hashicorp/vault-plugin-secrets-kv v0.5.5/go.mod h1:oNyUoMMQq6uNTwyYPnkldiedaknYbPfQIdKoyKQdy2g=
|
||||
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.1.2/go.mod h1:YRW9zn9NZNitRlPYNAWRp/YEdKCF/X8aOg8IYSxFT5Y=
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.3-0.20200518214608-746aba5fead6/go.mod h1:9Cy4Jp779BjuIOhYLjEfH3M3QCUxZgPnvJ3tAOOmof4=
|
||||
github.com/hashicorp/vault/api v1.0.1/go.mod h1:AV/+M5VPDpB90arloVX0rVDUIHkONiwz5Uza9HRtpUE=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519/go.mod h1:i9PKqwFko/s/aihU1uuHGh/FaQS+Xcgvd9dvnfAvQb0=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20191122173911-80fcc7907c78/go.mod h1:Uf8LaHyrYsgVgHzO2tMZKhqRGlL3UJ6XaSwW2EA1Iqo=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jcmturner/aescts v1.0.1/go.mod h1:k9gJoDUf1GH5r2IBtBjwjDCoLELYxOcEhitdP8RL7qQ=
|
||||
github.com/jcmturner/dnsutils v1.0.1/go.mod h1:tqMo38L01jO8AKxT0S9OQVlGZu3dkEt+z5CA+LOhwB0=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.0.0/go.mod h1:4/sqKY8Yzo/TIQ8MoCyk/EPcjb+czI9czxHcdXuZbFA=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2/go.mod h1:xkfESuHriIekR+4RoV+fu91j/CfnYM29Zi2tMFw5iD4=
|
||||
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
|
||||
github.com/jefferai/jsonx v1.0.0/go.mod h1:OGmqmi2tTeI/PS+qQfBDToLHHJIy/RMp24fPo8vFvoQ=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/joyent/triton-go v0.0.0-20190112182421-51ffac552869/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA=
|
||||
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/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.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
||||
github.com/michaelklishin/rabbit-hole v0.0.0-20191008194146-93d9988f0cd5/go.mod h1:+pmbihVqjC3GPdfWv1V2TnRSuVvwrWLKfEP/MZVB/Wc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mongodb/go-client-mongodb-atlas v0.1.2/go.mod h1:LS8O0YLkA+sbtOb3fZLF10yY3tJM+1xATXMJ3oU35LU=
|
||||
github.com/mwielbut/pointy v1.1.0/go.mod h1:MvvO+uMFj9T5DMda33HlvogsFBX7pWWKAkFIn4teYwY=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc/go.mod h1:1rLVY/DWf3U6vSZgH16S7pymfrhK2lcUlXjgGglw/lY=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/okta/okta-sdk-golang v1.0.1/go.mod h1:8k//sN2mFTq8Ayo90DqGbcumCkSmYjF0+2zkIbZysec=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/oracle/oci-go-sdk v12.5.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||
github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.2.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/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=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shirou/gopsutil v2.19.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
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/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.mongodb.org/mongo-driver v1.2.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
|
||||
go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190718200317-82a3ea8a504c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
|
||||
google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190513181449-d00d292a067c/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/ory-am/dockertest.v3 v3.3.4/go.mod h1:s9mmoLkaGeAh97qygnNj4xWkiN7e1SKekYC6CovU+ek=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20190409092523-d687e77c8ae9/go.mod h1:FQEUn50aaytlU65qqBn/w+5ugllHwrBzKm7DzbnXdzE=
|
||||
k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b/go.mod h1:FW86P8YXVLsbuplGMZeb20J3jYHscrDqw4jELaFJvRU=
|
||||
k8s.io/klog v0.0.0-20190306015804-8e90cee79f82/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||
layeh.com/radius v0.0.0-20190322222518-890bc1058917/go.mod h1:fywZKyu//X7iRzaxLgPWsvc0L26IUpVvE/aeIL2JtIQ=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLifetimeWatcherMissingInput = errors.New("missing input")
|
||||
ErrLifetimeWatcherMissingSecret = errors.New("missing secret")
|
||||
ErrLifetimeWatcherNotRenewable = errors.New("secret is not renewable")
|
||||
ErrLifetimeWatcherNoSecretData = errors.New("returned empty secret data")
|
||||
|
||||
// Deprecated; kept for compatibility
|
||||
ErrRenewerMissingInput = errors.New("missing input to renewer")
|
||||
ErrRenewerMissingSecret = errors.New("missing secret to renew")
|
||||
ErrRenewerNotRenewable = errors.New("secret is not renewable")
|
||||
ErrRenewerNoSecretData = errors.New("returned empty secret data")
|
||||
|
||||
// DefaultLifetimeWatcherRenewBuffer is the default size of the buffer for renew
|
||||
// messages on the channel.
|
||||
DefaultLifetimeWatcherRenewBuffer = 5
|
||||
// Deprecated: kept for backwards compatibility
|
||||
DefaultRenewerRenewBuffer = 5
|
||||
)
|
||||
|
||||
type RenewBehavior uint
|
||||
|
||||
const (
|
||||
// RenewBehaviorIgnoreErrors means we will attempt to keep renewing until
|
||||
// we hit the lifetime threshold. It also ignores errors stemming from
|
||||
// passing a non-renewable lease in. In practice, this means you simply
|
||||
// reauthenticate/refetch credentials when the watcher exits. This is the
|
||||
// default.
|
||||
RenewBehaviorIgnoreErrors RenewBehavior = iota
|
||||
|
||||
// RenewBehaviorRenewDisabled turns off renewal attempts entirely. This
|
||||
// allows you to simply watch lifetime and have the watcher return at a
|
||||
// reasonable threshold without actually making Vault calls.
|
||||
RenewBehaviorRenewDisabled
|
||||
|
||||
// RenewBehaviorErrorOnErrors is the "legacy" behavior which always exits
|
||||
// on some kind of error
|
||||
RenewBehaviorErrorOnErrors
|
||||
)
|
||||
|
||||
// LifetimeWatcher is a process for watching lifetime of a secret.
|
||||
//
|
||||
// watcher, err := client.NewLifetimeWatcher(&LifetimeWatcherInput{
|
||||
// Secret: mySecret,
|
||||
// })
|
||||
// go watcher.Start()
|
||||
// defer watcher.Stop()
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case err := <-watcher.DoneCh():
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Renewal is now over
|
||||
// case renewal := <-watcher.RenewCh():
|
||||
// log.Printf("Successfully renewed: %#v", renewal)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// `DoneCh` will return if renewal fails, or if the remaining lease duration is
|
||||
// under a built-in threshold and either renewing is not extending it or
|
||||
// renewing is disabled. In both cases, the caller should attempt a re-read of
|
||||
// the secret. Clients should check the return value of the channel to see if
|
||||
// renewal was successful.
|
||||
type LifetimeWatcher struct {
|
||||
l sync.Mutex
|
||||
|
||||
client *Client
|
||||
secret *Secret
|
||||
grace time.Duration
|
||||
random *rand.Rand
|
||||
increment int
|
||||
doneCh chan error
|
||||
renewCh chan *RenewOutput
|
||||
renewBehavior RenewBehavior
|
||||
|
||||
stopped bool
|
||||
stopCh chan struct{}
|
||||
|
||||
errLifetimeWatcherNotRenewable error
|
||||
errLifetimeWatcherNoSecretData error
|
||||
}
|
||||
|
||||
// LifetimeWatcherInput is used as input to the renew function.
|
||||
type LifetimeWatcherInput struct {
|
||||
// Secret is the secret to renew
|
||||
Secret *Secret
|
||||
|
||||
// DEPRECATED: this does not do anything.
|
||||
Grace time.Duration
|
||||
|
||||
// Rand is the randomizer to use for underlying randomization. If not
|
||||
// provided, one will be generated and seeded automatically. If provided, it
|
||||
// is assumed to have already been seeded.
|
||||
Rand *rand.Rand
|
||||
|
||||
// RenewBuffer is the size of the buffered channel where renew messages are
|
||||
// dispatched.
|
||||
RenewBuffer int
|
||||
|
||||
// The new TTL, in seconds, that should be set on the lease. The TTL set
|
||||
// here may or may not be honored by the vault server, based on Vault
|
||||
// configuration or any associated max TTL values.
|
||||
Increment int
|
||||
|
||||
// RenewBehavior controls what happens when a renewal errors or the
|
||||
// passed-in secret is not renewable.
|
||||
RenewBehavior RenewBehavior
|
||||
}
|
||||
|
||||
// RenewOutput is the metadata returned to the client (if it's listening) to
|
||||
// renew messages.
|
||||
type RenewOutput struct {
|
||||
// RenewedAt is the timestamp when the renewal took place (UTC).
|
||||
RenewedAt time.Time
|
||||
|
||||
// Secret is the underlying renewal data. It's the same struct as all data
|
||||
// that is returned from Vault, but since this is renewal data, it will not
|
||||
// usually include the secret itself.
|
||||
Secret *Secret
|
||||
}
|
||||
|
||||
// NewLifetimeWatcher creates a new renewer from the given input.
|
||||
func (c *Client) NewLifetimeWatcher(i *LifetimeWatcherInput) (*LifetimeWatcher, error) {
|
||||
if i == nil {
|
||||
return nil, ErrLifetimeWatcherMissingInput
|
||||
}
|
||||
|
||||
secret := i.Secret
|
||||
if secret == nil {
|
||||
return nil, ErrLifetimeWatcherMissingSecret
|
||||
}
|
||||
|
||||
random := i.Rand
|
||||
if random == nil {
|
||||
random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
}
|
||||
|
||||
renewBuffer := i.RenewBuffer
|
||||
if renewBuffer == 0 {
|
||||
renewBuffer = DefaultLifetimeWatcherRenewBuffer
|
||||
}
|
||||
|
||||
return &LifetimeWatcher{
|
||||
client: c,
|
||||
secret: secret,
|
||||
increment: i.Increment,
|
||||
random: random,
|
||||
doneCh: make(chan error, 1),
|
||||
renewCh: make(chan *RenewOutput, renewBuffer),
|
||||
renewBehavior: i.RenewBehavior,
|
||||
|
||||
stopped: false,
|
||||
stopCh: make(chan struct{}),
|
||||
|
||||
errLifetimeWatcherNotRenewable: ErrLifetimeWatcherNotRenewable,
|
||||
errLifetimeWatcherNoSecretData: ErrLifetimeWatcherNoSecretData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deprecated: exists only for backwards compatibility. Calls
|
||||
// NewLifetimeWatcher, and sets compatibility flags.
|
||||
func (c *Client) NewRenewer(i *LifetimeWatcherInput) (*LifetimeWatcher, error) {
|
||||
if i == nil {
|
||||
return nil, ErrRenewerMissingInput
|
||||
}
|
||||
|
||||
secret := i.Secret
|
||||
if secret == nil {
|
||||
return nil, ErrRenewerMissingSecret
|
||||
}
|
||||
|
||||
renewer, err := c.NewLifetimeWatcher(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
renewer.renewBehavior = RenewBehaviorErrorOnErrors
|
||||
renewer.errLifetimeWatcherNotRenewable = ErrRenewerNotRenewable
|
||||
renewer.errLifetimeWatcherNoSecretData = ErrRenewerNoSecretData
|
||||
return renewer, err
|
||||
}
|
||||
|
||||
// DoneCh returns the channel where the renewer will publish when renewal stops.
|
||||
// If there is an error, this will be an error.
|
||||
func (r *LifetimeWatcher) DoneCh() <-chan error {
|
||||
return r.doneCh
|
||||
}
|
||||
|
||||
// RenewCh is a channel that receives a message when a successful renewal takes
|
||||
// place and includes metadata about the renewal.
|
||||
func (r *LifetimeWatcher) RenewCh() <-chan *RenewOutput {
|
||||
return r.renewCh
|
||||
}
|
||||
|
||||
// Stop stops the renewer.
|
||||
func (r *LifetimeWatcher) Stop() {
|
||||
r.l.Lock()
|
||||
defer r.l.Unlock()
|
||||
|
||||
if !r.stopped {
|
||||
close(r.stopCh)
|
||||
r.stopped = true
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts a background process for watching the lifetime of this secret.
|
||||
// If renewal is enabled, when the secret has auth data, this attempts to renew
|
||||
// the auth (token); When the secret has a lease, this attempts to renew the
|
||||
// lease.
|
||||
func (r *LifetimeWatcher) Start() {
|
||||
r.doneCh <- r.doRenew()
|
||||
}
|
||||
|
||||
// Renew is for comnpatibility with the legacy api.Renewer. Calling Renew
|
||||
// simply chains to Start.
|
||||
func (r *LifetimeWatcher) Renew() {
|
||||
r.Start()
|
||||
}
|
||||
|
||||
// renewAuth is a helper for renewing authentication.
|
||||
func (r *LifetimeWatcher) doRenew() error {
|
||||
var nonRenewable bool
|
||||
var tokenMode bool
|
||||
var initLeaseDuration int
|
||||
var credString string
|
||||
var renewFunc func(string, int) (*Secret, error)
|
||||
|
||||
switch {
|
||||
case r.secret.Auth != nil:
|
||||
tokenMode = true
|
||||
nonRenewable = !r.secret.Auth.Renewable
|
||||
initLeaseDuration = r.secret.Auth.LeaseDuration
|
||||
credString = r.secret.Auth.ClientToken
|
||||
renewFunc = r.client.Auth().Token().RenewTokenAsSelf
|
||||
default:
|
||||
nonRenewable = !r.secret.Renewable
|
||||
initLeaseDuration = r.secret.LeaseDuration
|
||||
credString = r.secret.LeaseID
|
||||
renewFunc = r.client.Sys().Renew
|
||||
}
|
||||
|
||||
if credString == "" ||
|
||||
(nonRenewable && r.renewBehavior == RenewBehaviorErrorOnErrors) {
|
||||
return r.errLifetimeWatcherNotRenewable
|
||||
}
|
||||
|
||||
initialTime := time.Now()
|
||||
priorDuration := time.Duration(initLeaseDuration) * time.Second
|
||||
r.calculateGrace(priorDuration)
|
||||
|
||||
for {
|
||||
// Check if we are stopped.
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
var leaseDuration time.Duration
|
||||
fallbackLeaseDuration := initialTime.Add(priorDuration).Sub(time.Now())
|
||||
|
||||
switch {
|
||||
case nonRenewable || r.renewBehavior == RenewBehaviorRenewDisabled:
|
||||
// Can't or won't renew, just keep the same expiration so we exit
|
||||
// when it's reauthentication time
|
||||
leaseDuration = fallbackLeaseDuration
|
||||
|
||||
default:
|
||||
// Renew the token
|
||||
renewal, err := renewFunc(credString, r.increment)
|
||||
if err != nil || renewal == nil || (tokenMode && renewal.Auth == nil) {
|
||||
if r.renewBehavior == RenewBehaviorErrorOnErrors {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if renewal == nil || (tokenMode && renewal.Auth == nil) {
|
||||
return r.errLifetimeWatcherNoSecretData
|
||||
}
|
||||
}
|
||||
|
||||
leaseDuration = fallbackLeaseDuration
|
||||
break
|
||||
}
|
||||
|
||||
// Push a message that a renewal took place.
|
||||
select {
|
||||
case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
|
||||
default:
|
||||
}
|
||||
|
||||
// Possibly error if we are not renewable
|
||||
if ((tokenMode && !renewal.Auth.Renewable) || (!tokenMode && !renewal.Renewable)) &&
|
||||
r.renewBehavior == RenewBehaviorErrorOnErrors {
|
||||
return r.errLifetimeWatcherNotRenewable
|
||||
}
|
||||
|
||||
// Grab the lease duration
|
||||
newDuration := renewal.LeaseDuration
|
||||
if tokenMode {
|
||||
newDuration = renewal.Auth.LeaseDuration
|
||||
}
|
||||
|
||||
leaseDuration = time.Duration(newDuration) * time.Second
|
||||
}
|
||||
|
||||
// We keep evaluating a new grace period so long as the lease is
|
||||
// extending. Once it stops extending, we've hit the max and need to
|
||||
// rely on the grace duration.
|
||||
if leaseDuration > priorDuration {
|
||||
r.calculateGrace(leaseDuration)
|
||||
}
|
||||
priorDuration = leaseDuration
|
||||
|
||||
// The sleep duration is set to 2/3 of the current lease duration plus
|
||||
// 1/3 of the current grace period, which adds jitter.
|
||||
sleepDuration := time.Duration(float64(leaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
|
||||
|
||||
// If we are within grace, return now; or, if the amount of time we
|
||||
// would sleep would land us in the grace period. This helps with short
|
||||
// tokens; for example, you don't want a current lease duration of 4
|
||||
// seconds, a grace period of 3 seconds, and end up sleeping for more
|
||||
// than three of those seconds and having a very small budget of time
|
||||
// to renew.
|
||||
if leaseDuration <= r.grace || leaseDuration-sleepDuration <= r.grace {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
case <-time.After(sleepDuration):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sleepDuration calculates the time to sleep given the base lease duration. The
|
||||
// base is the resulting lease duration. It will be reduced to 1/3 and
|
||||
// multiplied by a random float between 0.0 and 1.0. This extra randomness
|
||||
// prevents multiple clients from all trying to renew simultaneously.
|
||||
func (r *LifetimeWatcher) sleepDuration(base time.Duration) time.Duration {
|
||||
sleep := float64(base)
|
||||
|
||||
// Renew at 1/3 the remaining lease. This will give us an opportunity to retry
|
||||
// at least one more time should the first renewal fail.
|
||||
sleep = sleep / 3.0
|
||||
|
||||
// Use a randomness so many clients do not hit Vault simultaneously.
|
||||
sleep = sleep * (r.random.Float64() + 1) / 2.0
|
||||
|
||||
return time.Duration(sleep)
|
||||
}
|
||||
|
||||
// calculateGrace calculates the grace period based on a reasonable set of
|
||||
// assumptions given the total lease time; it also adds some jitter to not have
|
||||
// clients be in sync.
|
||||
func (r *LifetimeWatcher) calculateGrace(leaseDuration time.Duration) {
|
||||
if leaseDuration == 0 {
|
||||
r.grace = 0
|
||||
return
|
||||
}
|
||||
|
||||
leaseNanos := float64(leaseDuration.Nanoseconds())
|
||||
jitterMax := 0.1 * leaseNanos
|
||||
|
||||
// For a given lease duration, we want to allow 80-90% of that to elapse,
|
||||
// so the remaining amount is the grace period
|
||||
r.grace = time.Duration(jitterMax) + time.Duration(uint64(r.random.Int63())%uint64(jitterMax))
|
||||
}
|
||||
|
||||
type Renewer = LifetimeWatcher
|
||||
type RenewerInput = LifetimeWatcherInput
|
|
@ -21,8 +21,9 @@ var (
|
|||
// changed
|
||||
DefaultWrappingTTL = "5m"
|
||||
|
||||
// The default function used if no other function is set, which honors the
|
||||
// env var and wraps `sys/wrapping/wrap`
|
||||
// The default function used if no other function is set. It honors the env
|
||||
// var to set the wrap TTL. The default wrap TTL will apply when when writing
|
||||
// to `sys/wrapping/wrap` when the env var is not set.
|
||||
DefaultWrappingLookupFunc = func(operation, path string) string {
|
||||
if os.Getenv(EnvVaultWrapTTL) != "" {
|
||||
return os.Getenv(EnvVaultWrapTTL)
|
||||
|
@ -134,9 +135,20 @@ func (c *Logical) Write(path string, data map[string]interface{}) (*Secret, erro
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return c.write(path, r)
|
||||
}
|
||||
|
||||
func (c *Logical) WriteBytes(path string, data []byte) (*Secret, error) {
|
||||
r := c.c.NewRequest("PUT", "/v1/"+path)
|
||||
r.BodyBytes = data
|
||||
|
||||
return c.write(path, r)
|
||||
}
|
||||
|
||||
func (c *Logical) write(path string, request *Request) (*Secret, error) {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
resp, err := c.c.RawRequestWithContext(ctx, r)
|
||||
resp, err := c.c.RawRequestWithContext(ctx, request)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
|
|
@ -118,6 +118,9 @@ func VaultPluginTLSProvider(apiTLSConfig *TLSConfig) func() (*tls.Config, error)
|
|||
return nil, errwrap.Wrapf("error during api client creation: {{err}}", err)
|
||||
}
|
||||
|
||||
// Reset token value to make sure nothing has been set by default
|
||||
client.ClearToken()
|
||||
|
||||
secret, err := client.Logical().Unwrap(unwrapToken)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error during token unwrap request: {{err}}", err)
|
||||
|
|
|
@ -1,349 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRenewerMissingInput = errors.New("missing input to renewer")
|
||||
ErrRenewerMissingSecret = errors.New("missing secret to renew")
|
||||
ErrRenewerNotRenewable = errors.New("secret is not renewable")
|
||||
ErrRenewerNoSecretData = errors.New("returned empty secret data")
|
||||
|
||||
// DefaultRenewerRenewBuffer is the default size of the buffer for renew
|
||||
// messages on the channel.
|
||||
DefaultRenewerRenewBuffer = 5
|
||||
)
|
||||
|
||||
// Renewer is a process for renewing a secret.
|
||||
//
|
||||
// renewer, err := client.NewRenewer(&RenewerInput{
|
||||
// Secret: mySecret,
|
||||
// })
|
||||
// go renewer.Renew()
|
||||
// defer renewer.Stop()
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case err := <-renewer.DoneCh():
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Renewal is now over
|
||||
// case renewal := <-renewer.RenewCh():
|
||||
// log.Printf("Successfully renewed: %#v", renewal)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// The `DoneCh` will return if renewal fails or if the remaining lease duration
|
||||
// after a renewal is less than or equal to the grace (in number of seconds). In
|
||||
// both cases, the caller should attempt a re-read of the secret. Clients should
|
||||
// check the return value of the channel to see if renewal was successful.
|
||||
type Renewer struct {
|
||||
l sync.Mutex
|
||||
|
||||
client *Client
|
||||
secret *Secret
|
||||
grace time.Duration
|
||||
random *rand.Rand
|
||||
increment int
|
||||
doneCh chan error
|
||||
renewCh chan *RenewOutput
|
||||
|
||||
stopped bool
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// RenewerInput is used as input to the renew function.
|
||||
type RenewerInput struct {
|
||||
// Secret is the secret to renew
|
||||
Secret *Secret
|
||||
|
||||
// DEPRECATED: this does not do anything.
|
||||
Grace time.Duration
|
||||
|
||||
// Rand is the randomizer to use for underlying randomization. If not
|
||||
// provided, one will be generated and seeded automatically. If provided, it
|
||||
// is assumed to have already been seeded.
|
||||
Rand *rand.Rand
|
||||
|
||||
// RenewBuffer is the size of the buffered channel where renew messages are
|
||||
// dispatched.
|
||||
RenewBuffer int
|
||||
|
||||
// The new TTL, in seconds, that should be set on the lease. The TTL set
|
||||
// here may or may not be honored by the vault server, based on Vault
|
||||
// configuration or any associated max TTL values.
|
||||
Increment int
|
||||
}
|
||||
|
||||
// RenewOutput is the metadata returned to the client (if it's listening) to
|
||||
// renew messages.
|
||||
type RenewOutput struct {
|
||||
// RenewedAt is the timestamp when the renewal took place (UTC).
|
||||
RenewedAt time.Time
|
||||
|
||||
// Secret is the underlying renewal data. It's the same struct as all data
|
||||
// that is returned from Vault, but since this is renewal data, it will not
|
||||
// usually include the secret itself.
|
||||
Secret *Secret
|
||||
}
|
||||
|
||||
// NewRenewer creates a new renewer from the given input.
|
||||
func (c *Client) NewRenewer(i *RenewerInput) (*Renewer, error) {
|
||||
if i == nil {
|
||||
return nil, ErrRenewerMissingInput
|
||||
}
|
||||
|
||||
secret := i.Secret
|
||||
if secret == nil {
|
||||
return nil, ErrRenewerMissingSecret
|
||||
}
|
||||
|
||||
random := i.Rand
|
||||
if random == nil {
|
||||
random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
}
|
||||
|
||||
renewBuffer := i.RenewBuffer
|
||||
if renewBuffer == 0 {
|
||||
renewBuffer = DefaultRenewerRenewBuffer
|
||||
}
|
||||
|
||||
return &Renewer{
|
||||
client: c,
|
||||
secret: secret,
|
||||
increment: i.Increment,
|
||||
random: random,
|
||||
doneCh: make(chan error, 1),
|
||||
renewCh: make(chan *RenewOutput, renewBuffer),
|
||||
|
||||
stopped: false,
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DoneCh returns the channel where the renewer will publish when renewal stops.
|
||||
// If there is an error, this will be an error.
|
||||
func (r *Renewer) DoneCh() <-chan error {
|
||||
return r.doneCh
|
||||
}
|
||||
|
||||
// RenewCh is a channel that receives a message when a successful renewal takes
|
||||
// place and includes metadata about the renewal.
|
||||
func (r *Renewer) RenewCh() <-chan *RenewOutput {
|
||||
return r.renewCh
|
||||
}
|
||||
|
||||
// Stop stops the renewer.
|
||||
func (r *Renewer) Stop() {
|
||||
r.l.Lock()
|
||||
if !r.stopped {
|
||||
close(r.stopCh)
|
||||
r.stopped = true
|
||||
}
|
||||
r.l.Unlock()
|
||||
}
|
||||
|
||||
// Renew starts a background process for renewing this secret. When the secret
|
||||
// has auth data, this attempts to renew the auth (token). When the secret has
|
||||
// a lease, this attempts to renew the lease.
|
||||
func (r *Renewer) Renew() {
|
||||
var result error
|
||||
if r.secret.Auth != nil {
|
||||
result = r.renewAuth()
|
||||
} else {
|
||||
result = r.renewLease()
|
||||
}
|
||||
|
||||
r.doneCh <- result
|
||||
}
|
||||
|
||||
// renewAuth is a helper for renewing authentication.
|
||||
func (r *Renewer) renewAuth() error {
|
||||
if !r.secret.Auth.Renewable || r.secret.Auth.ClientToken == "" {
|
||||
return ErrRenewerNotRenewable
|
||||
}
|
||||
|
||||
priorDuration := time.Duration(r.secret.Auth.LeaseDuration) * time.Second
|
||||
r.calculateGrace(priorDuration)
|
||||
|
||||
client, token := r.client, r.secret.Auth.ClientToken
|
||||
|
||||
for {
|
||||
// Check if we are stopped.
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Renew the auth.
|
||||
renewal, err := client.Auth().Token().RenewTokenAsSelf(token, r.increment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Push a message that a renewal took place.
|
||||
select {
|
||||
case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
|
||||
default:
|
||||
}
|
||||
|
||||
// Somehow, sometimes, this happens.
|
||||
if renewal == nil || renewal.Auth == nil {
|
||||
return ErrRenewerNoSecretData
|
||||
}
|
||||
|
||||
// Do nothing if we are not renewable
|
||||
if !renewal.Auth.Renewable {
|
||||
return ErrRenewerNotRenewable
|
||||
}
|
||||
|
||||
// Grab the lease duration
|
||||
leaseDuration := time.Duration(renewal.Auth.LeaseDuration) * time.Second
|
||||
|
||||
// We keep evaluating a new grace period so long as the lease is
|
||||
// extending. Once it stops extending, we've hit the max and need to
|
||||
// rely on the grace duration.
|
||||
if leaseDuration > priorDuration {
|
||||
r.calculateGrace(leaseDuration)
|
||||
}
|
||||
priorDuration = leaseDuration
|
||||
|
||||
// The sleep duration is set to 2/3 of the current lease duration plus
|
||||
// 1/3 of the current grace period, which adds jitter.
|
||||
sleepDuration := time.Duration(float64(leaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
|
||||
|
||||
// If we are within grace, return now; or, if the amount of time we
|
||||
// would sleep would land us in the grace period. This helps with short
|
||||
// tokens; for example, you don't want a current lease duration of 4
|
||||
// seconds, a grace period of 3 seconds, and end up sleeping for more
|
||||
// than three of those seconds and having a very small budget of time
|
||||
// to renew.
|
||||
if leaseDuration <= r.grace || leaseDuration-sleepDuration <= r.grace {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
case <-time.After(sleepDuration):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renewLease is a helper for renewing a lease.
|
||||
func (r *Renewer) renewLease() error {
|
||||
if !r.secret.Renewable || r.secret.LeaseID == "" {
|
||||
return ErrRenewerNotRenewable
|
||||
}
|
||||
|
||||
priorDuration := time.Duration(r.secret.LeaseDuration) * time.Second
|
||||
r.calculateGrace(priorDuration)
|
||||
|
||||
client, leaseID := r.client, r.secret.LeaseID
|
||||
|
||||
for {
|
||||
// Check if we are stopped.
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Renew the lease.
|
||||
renewal, err := client.Sys().Renew(leaseID, r.increment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Push a message that a renewal took place.
|
||||
select {
|
||||
case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
|
||||
default:
|
||||
}
|
||||
|
||||
// Somehow, sometimes, this happens.
|
||||
if renewal == nil {
|
||||
return ErrRenewerNoSecretData
|
||||
}
|
||||
|
||||
// Do nothing if we are not renewable
|
||||
if !renewal.Renewable {
|
||||
return ErrRenewerNotRenewable
|
||||
}
|
||||
|
||||
// Grab the lease duration
|
||||
leaseDuration := time.Duration(renewal.LeaseDuration) * time.Second
|
||||
|
||||
// We keep evaluating a new grace period so long as the lease is
|
||||
// extending. Once it stops extending, we've hit the max and need to
|
||||
// rely on the grace duration.
|
||||
if leaseDuration > priorDuration {
|
||||
r.calculateGrace(leaseDuration)
|
||||
}
|
||||
priorDuration = leaseDuration
|
||||
|
||||
// The sleep duration is set to 2/3 of the current lease duration plus
|
||||
// 1/3 of the current grace period, which adds jitter.
|
||||
sleepDuration := time.Duration(float64(leaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
|
||||
|
||||
// If we are within grace, return now; or, if the amount of time we
|
||||
// would sleep would land us in the grace period. This helps with short
|
||||
// tokens; for example, you don't want a current lease duration of 4
|
||||
// seconds, a grace period of 3 seconds, and end up sleeping for more
|
||||
// than three of those seconds and having a very small budget of time
|
||||
// to renew.
|
||||
if leaseDuration <= r.grace || leaseDuration-sleepDuration <= r.grace {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
case <-time.After(sleepDuration):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sleepDuration calculates the time to sleep given the base lease duration. The
|
||||
// base is the resulting lease duration. It will be reduced to 1/3 and
|
||||
// multiplied by a random float between 0.0 and 1.0. This extra randomness
|
||||
// prevents multiple clients from all trying to renew simultaneously.
|
||||
func (r *Renewer) sleepDuration(base time.Duration) time.Duration {
|
||||
sleep := float64(base)
|
||||
|
||||
// Renew at 1/3 the remaining lease. This will give us an opportunity to retry
|
||||
// at least one more time should the first renewal fail.
|
||||
sleep = sleep / 3.0
|
||||
|
||||
// Use a randomness so many clients do not hit Vault simultaneously.
|
||||
sleep = sleep * (r.random.Float64() + 1) / 2.0
|
||||
|
||||
return time.Duration(sleep)
|
||||
}
|
||||
|
||||
// calculateGrace calculates the grace period based on a reasonable set of
|
||||
// assumptions given the total lease time; it also adds some jitter to not have
|
||||
// clients be in sync.
|
||||
func (r *Renewer) calculateGrace(leaseDuration time.Duration) {
|
||||
if leaseDuration == 0 {
|
||||
r.grace = 0
|
||||
return
|
||||
}
|
||||
|
||||
leaseNanos := float64(leaseDuration.Nanoseconds())
|
||||
jitterMax := 0.1 * leaseNanos
|
||||
|
||||
// For a given lease duration, we want to allow 80-90% of that to elapse,
|
||||
// so the remaining amount is the grace period
|
||||
r.grace = time.Duration(jitterMax) + time.Duration(uint64(r.random.Int63())%uint64(jitterMax))
|
||||
}
|
|
@ -18,6 +18,7 @@ import (
|
|||
type Request struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Host string
|
||||
Params url.Values
|
||||
Headers http.Header
|
||||
ClientToken string
|
||||
|
@ -115,7 +116,7 @@ func (r *Request) toRetryableHTTP() (*retryablehttp.Request, error) {
|
|||
req.URL.User = r.URL.User
|
||||
req.URL.Scheme = r.URL.Scheme
|
||||
req.URL.Host = r.URL.Host
|
||||
req.Host = r.URL.Host
|
||||
req.Host = r.Host
|
||||
|
||||
if r.Headers != nil {
|
||||
for header, vals := range r.Headers {
|
||||
|
|
|
@ -27,8 +27,8 @@ func (r *Response) DecodeJSON(out interface{}) error {
|
|||
// body must still be closed manually.
|
||||
func (r *Response) Error() error {
|
||||
// 200 to 399 are okay status codes. 429 is the code for health status of
|
||||
// standby nodes.
|
||||
if (r.StatusCode >= 200 && r.StatusCode < 400) || r.StatusCode == 429 {
|
||||
// standby nodes, otherwise, 429 is treated as quota limit reached.
|
||||
if (r.StatusCode >= 200 && r.StatusCode < 400) || (r.StatusCode == 429 && r.Request.URL.Path == "/v1/sys/health") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ type SSHVerifyResponse struct {
|
|||
type SSHHelperConfig struct {
|
||||
VaultAddr string `hcl:"vault_addr"`
|
||||
SSHMountPoint string `hcl:"ssh_mount_point"`
|
||||
Namespace string `hcl:"namespace"`
|
||||
CACert string `hcl:"ca_cert"`
|
||||
CAPath string `hcl:"ca_path"`
|
||||
AllowedCidrList string `hcl:"allowed_cidr_list"`
|
||||
|
@ -123,6 +124,11 @@ func (c *SSHHelperConfig) NewClient() (*Client, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Configure namespace
|
||||
if c.Namespace != "" {
|
||||
client.SetNamespace(c.Namespace)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
|
@ -155,6 +161,7 @@ func ParseSSHHelperConfig(contents string) (*SSHHelperConfig, error) {
|
|||
valid := []string{
|
||||
"vault_addr",
|
||||
"ssh_mount_point",
|
||||
"namespace",
|
||||
"ca_cert",
|
||||
"ca_path",
|
||||
"allowed_cidr_list",
|
||||
|
|
|
@ -116,7 +116,7 @@ func (c *Sys) DisableAudit(path string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Structures for the requests/resposne are all down here. They aren't
|
||||
// Structures for the requests/response are all down here. They aren't
|
||||
// individually documented because the map almost directly to the raw HTTP API
|
||||
// documentation. Please refer to that documentation for more details.
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ func (c *Sys) GenerateDROperationTokenStatus() (*GenerateRootStatusResponse, err
|
|||
return c.generateRootStatusCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt")
|
||||
}
|
||||
|
||||
func (c *Sys) GenerateRecoveryOperationTokenStatus() (*GenerateRootStatusResponse, error) {
|
||||
return c.generateRootStatusCommon("/v1/sys/generate-recovery-token/attempt")
|
||||
}
|
||||
|
||||
func (c *Sys) generateRootStatusCommon(path string) (*GenerateRootStatusResponse, error) {
|
||||
r := c.c.NewRequest("GET", path)
|
||||
|
||||
|
@ -34,6 +38,10 @@ func (c *Sys) GenerateDROperationTokenInit(otp, pgpKey string) (*GenerateRootSta
|
|||
return c.generateRootInitCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt", otp, pgpKey)
|
||||
}
|
||||
|
||||
func (c *Sys) GenerateRecoveryOperationTokenInit(otp, pgpKey string) (*GenerateRootStatusResponse, error) {
|
||||
return c.generateRootInitCommon("/v1/sys/generate-recovery-token/attempt", otp, pgpKey)
|
||||
}
|
||||
|
||||
func (c *Sys) generateRootInitCommon(path, otp, pgpKey string) (*GenerateRootStatusResponse, error) {
|
||||
body := map[string]interface{}{
|
||||
"otp": otp,
|
||||
|
@ -66,6 +74,10 @@ func (c *Sys) GenerateDROperationTokenCancel() error {
|
|||
return c.generateRootCancelCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt")
|
||||
}
|
||||
|
||||
func (c *Sys) GenerateRecoveryOperationTokenCancel() error {
|
||||
return c.generateRootCancelCommon("/v1/sys/generate-recovery-token/attempt")
|
||||
}
|
||||
|
||||
func (c *Sys) generateRootCancelCommon(path string) error {
|
||||
r := c.c.NewRequest("DELETE", path)
|
||||
|
||||
|
@ -86,6 +98,10 @@ func (c *Sys) GenerateDROperationTokenUpdate(shard, nonce string) (*GenerateRoot
|
|||
return c.generateRootUpdateCommon("/v1/sys/replication/dr/secondary/generate-operation-token/update", shard, nonce)
|
||||
}
|
||||
|
||||
func (c *Sys) GenerateRecoveryOperationTokenUpdate(shard, nonce string) (*GenerateRootStatusResponse, error) {
|
||||
return c.generateRootUpdateCommon("/v1/sys/generate-recovery-token/update", shard, nonce)
|
||||
}
|
||||
|
||||
func (c *Sys) generateRootUpdateCommon(path, shard, nonce string) (*GenerateRootStatusResponse, error) {
|
||||
body := map[string]interface{}{
|
||||
"key": shard,
|
||||
|
|
|
@ -26,4 +26,6 @@ type LeaderResponse struct {
|
|||
PerfStandby bool `json:"performance_standby"`
|
||||
PerfStandbyLastRemoteWAL uint64 `json:"performance_standby_last_remote_wal"`
|
||||
LastWAL uint64 `json:"last_wal"`
|
||||
RaftCommittedIndex uint64 `json:"raft_committed_index,omitempty"`
|
||||
RaftAppliedIndex uint64 `json:"raft_applied_index,omitempty"`
|
||||
}
|
||||
|
|
|
@ -28,7 +28,13 @@ func (c *Sys) Renew(id string, increment int) (*Secret, error) {
|
|||
}
|
||||
|
||||
func (c *Sys) Revoke(id string) error {
|
||||
r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke/"+id)
|
||||
r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke")
|
||||
body := map[string]interface{}{
|
||||
"lease_id": id,
|
||||
}
|
||||
if err := r.SetJSONBody(body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Monitor returns a channel that outputs strings containing the log messages
|
||||
// coming from the server.
|
||||
func (c *Sys) Monitor(ctx context.Context, logLevel string) (chan string, error) {
|
||||
r := c.c.NewRequest("GET", "/v1/sys/monitor")
|
||||
|
||||
if logLevel == "" {
|
||||
r.Params.Add("log_level", "info")
|
||||
} else {
|
||||
r.Params.Add("log_level", logLevel)
|
||||
}
|
||||
|
||||
resp, err := c.c.RawRequestWithContext(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logCh := make(chan string, 64)
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
droppedCount := 0
|
||||
|
||||
defer close(logCh)
|
||||
defer resp.Body.Close()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !scanner.Scan() {
|
||||
return
|
||||
}
|
||||
|
||||
logMessage := scanner.Text()
|
||||
|
||||
if droppedCount > 0 {
|
||||
select {
|
||||
case logCh <- fmt.Sprintf("Monitor dropped %d logs during monitor request\n", droppedCount):
|
||||
droppedCount = 0
|
||||
default:
|
||||
droppedCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case logCh <- logMessage:
|
||||
default:
|
||||
droppedCount++
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return logCh, nil
|
||||
}
|
|
@ -129,12 +129,13 @@ func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) {
|
|||
}
|
||||
|
||||
type MountInput struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Config MountConfigInput `json:"config"`
|
||||
Local bool `json:"local"`
|
||||
SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"`
|
||||
Options map[string]string `json:"options"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Config MountConfigInput `json:"config"`
|
||||
Local bool `json:"local"`
|
||||
SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"`
|
||||
ExternalEntropyAccess bool `json:"external_entropy_access" mapstructure:"external_entropy_access"`
|
||||
Options map[string]string `json:"options"`
|
||||
|
||||
// Deprecated: Newer server responses should be returning this information in the
|
||||
// Type field (json: "type") instead.
|
||||
|
@ -159,14 +160,15 @@ type MountConfigInput struct {
|
|||
}
|
||||
|
||||
type MountOutput struct {
|
||||
UUID string `json:"uuid"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Accessor string `json:"accessor"`
|
||||
Config MountConfigOutput `json:"config"`
|
||||
Options map[string]string `json:"options"`
|
||||
Local bool `json:"local"`
|
||||
SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"`
|
||||
UUID string `json:"uuid"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Accessor string `json:"accessor"`
|
||||
Config MountConfigOutput `json:"config"`
|
||||
Options map[string]string `json:"options"`
|
||||
Local bool `json:"local"`
|
||||
SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"`
|
||||
ExternalEntropyAccess bool `json:"external_entropy_access" mapstructure:"external_entropy_access"`
|
||||
}
|
||||
|
||||
type MountConfigOutput struct {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
@ -225,6 +226,106 @@ func (c *Sys) DeregisterPlugin(i *DeregisterPluginInput) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ReloadPluginInput is used as input to the ReloadPlugin function.
|
||||
type ReloadPluginInput struct {
|
||||
// Plugin is the name of the plugin to reload, as registered in the plugin catalog
|
||||
Plugin string `json:"plugin"`
|
||||
|
||||
// Mounts is the array of string mount paths of the plugin backends to reload
|
||||
Mounts []string `json:"mounts"`
|
||||
|
||||
// Scope is the scope of the plugin reload
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// ReloadPlugin reloads mounted plugin backends, possibly returning
|
||||
// reloadId for a cluster scoped reload
|
||||
func (c *Sys) ReloadPlugin(i *ReloadPluginInput) (string, error) {
|
||||
path := "/v1/sys/plugins/reload/backend"
|
||||
req := c.c.NewRequest(http.MethodPut, path)
|
||||
|
||||
if err := req.SetJSONBody(i); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
resp, err := c.c.RawRequestWithContext(ctx, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if i.Scope == "global" {
|
||||
// Get the reload id
|
||||
secret, parseErr := ParseSecret(resp.Body)
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
if _, ok := secret.Data["reload_id"]; ok {
|
||||
return secret.Data["reload_id"].(string), nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// ReloadStatus is the status of an individual node's plugin reload
|
||||
type ReloadStatus struct {
|
||||
Timestamp time.Time `json:"timestamp" mapstructure:"timestamp"`
|
||||
Error string `json:"error" mapstructure:"error"`
|
||||
}
|
||||
|
||||
// ReloadStatusResponse is the combined response of all known completed plugin reloads
|
||||
type ReloadStatusResponse struct {
|
||||
ReloadID string `mapstructure:"reload_id"`
|
||||
Results map[string]*ReloadStatus `mapstructure:"results"`
|
||||
}
|
||||
|
||||
// ReloadPluginStatusInput is used as input to the ReloadStatusPlugin function.
|
||||
type ReloadPluginStatusInput struct {
|
||||
// ReloadID is the ID of the reload operation
|
||||
ReloadID string `json:"reload_id"`
|
||||
}
|
||||
|
||||
// ReloadPluginStatus retrieves the status of a reload operation
|
||||
func (c *Sys) ReloadPluginStatus(reloadStatusInput *ReloadPluginStatusInput) (*ReloadStatusResponse, error) {
|
||||
path := "/v1/sys/plugins/reload/backend/status"
|
||||
req := c.c.NewRequest(http.MethodGet, path)
|
||||
req.Params.Add("reload_id", reloadStatusInput.ReloadID)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
resp, err := c.c.RawRequestWithContext(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp != nil {
|
||||
secret, parseErr := ParseSecret(resp.Body)
|
||||
if parseErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r ReloadStatusResponse
|
||||
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
Result: &r,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = d.Decode(secret.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
// catalogPathByType is a helper to construct the proper API path by plugin type
|
||||
func catalogPathByType(pluginType consts.PluginType, name string) string {
|
||||
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s/%s", pluginType, name)
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
|
@ -16,10 +17,11 @@ type RaftJoinResponse struct {
|
|||
// RaftJoinRequest represents the parameters consumed by the raft join API
|
||||
type RaftJoinRequest struct {
|
||||
LeaderAPIAddr string `json:"leader_api_addr"`
|
||||
LeaderCACert string `json:"leader_ca_cert":`
|
||||
LeaderCACert string `json:"leader_ca_cert"`
|
||||
LeaderClientCert string `json:"leader_client_cert"`
|
||||
LeaderClientKey string `json:"leader_client_key"`
|
||||
Retry bool `json:"retry"`
|
||||
NonVoter bool `json:"non_voter"`
|
||||
}
|
||||
|
||||
// RaftJoin adds the node from which this call is invoked from to the raft
|
||||
|
@ -90,10 +92,37 @@ func (c *Sys) RaftSnapshot(snapWriter io.Writer) error {
|
|||
// to determine if the body contains error message.
|
||||
var result *Response
|
||||
resp, err := c.c.config.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for a redirect, only allowing for a single redirect
|
||||
if resp.StatusCode == 301 || resp.StatusCode == 302 || resp.StatusCode == 307 {
|
||||
// Parse the updated location
|
||||
respLoc, err := resp.Location()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure a protocol downgrade doesn't happen
|
||||
if req.URL.Scheme == "https" && respLoc.Scheme != "https" {
|
||||
return fmt.Errorf("redirect would cause protocol downgrade")
|
||||
}
|
||||
|
||||
// Update the request
|
||||
req.URL = respLoc
|
||||
|
||||
// Retry the request
|
||||
resp, err = c.c.config.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
result = &Response{Response: resp}
|
||||
if err := result.Error(); err != nil {
|
||||
return err
|
||||
|
|
|
@ -77,6 +77,7 @@ type SealStatusResponse struct {
|
|||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
ClusterID string `json:"cluster_id,omitempty"`
|
||||
RecoverySeal bool `json:"recovery_seal"`
|
||||
StorageType string `json:"storage_type,omitempty"`
|
||||
}
|
||||
|
||||
type UnsealOpts struct {
|
||||
|
|
|
@ -12,12 +12,15 @@ const (
|
|||
// AuthHeaderName is the name of the header containing the token.
|
||||
AuthHeaderName = "X-Vault-Token"
|
||||
|
||||
// RequestHeaderName is the name of the header used by the Agent for
|
||||
// SSRF protection.
|
||||
RequestHeaderName = "X-Vault-Request"
|
||||
|
||||
// PerformanceReplicationALPN is the negotiated protocol used for
|
||||
// performance replication.
|
||||
PerformanceReplicationALPN = "replication_v1"
|
||||
|
||||
// DRReplicationALPN is the negotiated protocol used for
|
||||
// dr replication.
|
||||
// DRReplicationALPN is the negotiated protocol used for dr replication.
|
||||
DRReplicationALPN = "replication_dr_v1"
|
||||
|
||||
PerfStandbyALPN = "perf_standby_v1"
|
||||
|
@ -25,4 +28,8 @@ const (
|
|||
RequestForwardingALPN = "req_fw_sb-act_v1"
|
||||
|
||||
RaftStorageALPN = "raft_storage_v1"
|
||||
|
||||
// ReplicationResolverALPN is the negotiated protocol used for
|
||||
// resolving replicaiton addresses
|
||||
ReplicationResolverALPN = "replication_resolver_v1"
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@ const (
|
|||
// manager. It should contain a character that is not allowed in secondary
|
||||
// ids to ensure it doesn't collide.
|
||||
CurrentReplicatedSecondaryIdentifier = ".current"
|
||||
CoreFeatureFlagPath = "core/cluster/feature-flags"
|
||||
)
|
||||
|
||||
type ReplicationState uint32
|
||||
|
@ -144,6 +145,10 @@ func (r ReplicationState) GetPerformanceString() string {
|
|||
}
|
||||
}
|
||||
|
||||
func (r ReplicationState) IsPrimaryState() bool {
|
||||
return r.HasState(ReplicationPerformancePrimary | ReplicationDRPrimary)
|
||||
}
|
||||
|
||||
func (r ReplicationState) HasState(flag ReplicationState) bool { return r&flag != 0 }
|
||||
func (r *ReplicationState) AddState(flag ReplicationState) { *r |= flag }
|
||||
func (r *ReplicationState) ClearState(flag ReplicationState) { *r &= ^flag }
|
||||
|
|
|
@ -30,4 +30,5 @@ Temporary Items
|
|||
|
||||
# End of https://www.gitignore.io/api/macos
|
||||
|
||||
lz4c/lz4c
|
||||
cmd/*/*exe
|
||||
.idea
|
|
@ -1,9 +1,13 @@
|
|||
language: go
|
||||
|
||||
env:
|
||||
- GO111MODULE=off
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- master
|
||||
|
||||
matrix:
|
||||
|
@ -16,3 +20,5 @@ sudo: false
|
|||
script:
|
||||
- go test -v -cpu=2
|
||||
- go test -v -cpu=2 -race
|
||||
- go test -v -cpu=2 -tags noasm
|
||||
- go test -v -cpu=2 -race -tags noasm
|
||||
|
|
|
@ -1,24 +1,90 @@
|
|||
[![godoc](https://godoc.org/github.com/pierrec/lz4?status.png)](https://godoc.org/github.com/pierrec/lz4)
|
||||
# lz4 : LZ4 compression in pure Go
|
||||
|
||||
# lz4
|
||||
LZ4 compression and decompression in pure Go.
|
||||
[![GoDoc](https://godoc.org/github.com/pierrec/lz4?status.svg)](https://godoc.org/github.com/pierrec/lz4)
|
||||
[![Build Status](https://travis-ci.org/pierrec/lz4.svg?branch=master)](https://travis-ci.org/pierrec/lz4)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/pierrec/lz4)](https://goreportcard.com/report/github.com/pierrec/lz4)
|
||||
[![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/pierrec/lz4.svg?style=social)](https://github.com/pierrec/lz4/tags)
|
||||
|
||||
## Usage
|
||||
## Overview
|
||||
|
||||
```go
|
||||
import "github.com/pierrec/lz4"
|
||||
This package provides a streaming interface to [LZ4 data streams](http://fastcompression.blogspot.fr/2013/04/lz4-streaming-format-final.html) as well as low level compress and uncompress functions for LZ4 data blocks.
|
||||
The implementation is based on the reference C [one](https://github.com/lz4/lz4).
|
||||
|
||||
## Install
|
||||
|
||||
Assuming you have the go toolchain installed:
|
||||
|
||||
```
|
||||
go get github.com/pierrec/lz4
|
||||
```
|
||||
|
||||
## Description
|
||||
Package lz4 implements reading and writing lz4 compressed data (a frame),
|
||||
as specified in http://fastcompression.blogspot.fr/2013/04/lz4-streaming-format-final.html.
|
||||
There is a command line interface tool to compress and decompress LZ4 files.
|
||||
|
||||
This package is **compatible with the LZ4 frame format** although the block level compression
|
||||
and decompression functions are exposed and are fully compatible with the lz4 block format
|
||||
definition, they are low level and should not be used directly.
|
||||
```
|
||||
go install github.com/pierrec/lz4/cmd/lz4c
|
||||
```
|
||||
|
||||
For a complete description of an lz4 compressed block, see:
|
||||
http://fastcompression.blogspot.fr/2011/05/lz4-explained.html
|
||||
Usage
|
||||
|
||||
See https://github.com/Cyan4973/lz4 for the reference C implementation.
|
||||
```
|
||||
Usage of lz4c:
|
||||
-version
|
||||
print the program version
|
||||
|
||||
Subcommands:
|
||||
Compress the given files or from stdin to stdout.
|
||||
compress [arguments] [<file name> ...]
|
||||
-bc
|
||||
enable block checksum
|
||||
-l int
|
||||
compression level (0=fastest)
|
||||
-sc
|
||||
disable stream checksum
|
||||
-size string
|
||||
block max size [64K,256K,1M,4M] (default "4M")
|
||||
|
||||
Uncompress the given files or from stdin to stdout.
|
||||
uncompress [arguments] [<file name> ...]
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
// Compress and uncompress an input string.
|
||||
s := "hello world"
|
||||
r := strings.NewReader(s)
|
||||
|
||||
// The pipe will uncompress the data from the writer.
|
||||
pr, pw := io.Pipe()
|
||||
zw := lz4.NewWriter(pw)
|
||||
zr := lz4.NewReader(pr)
|
||||
|
||||
go func() {
|
||||
// Compress the input string.
|
||||
_, _ = io.Copy(zw, r)
|
||||
_ = zw.Close() // Make sure the writer is closed
|
||||
_ = pw.Close() // Terminate the pipe
|
||||
}()
|
||||
|
||||
_, _ = io.Copy(os.Stdout, zr)
|
||||
|
||||
// Output:
|
||||
// hello world
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome for bug fixing, performance improvements...!
|
||||
|
||||
- Open an issue with a proper description
|
||||
- Send a pull request with appropriate test case(s)
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all [contributors](https://github.com/pierrec/lz4/graphs/contributors) so far!
|
||||
|
||||
Special thanks to [@Zariel](https://github.com/Zariel) for his asm implementation of the decoder.
|
||||
|
||||
Special thanks to [@klauspost](https://github.com/klauspost) for his work on optimizing the code.
|
||||
|
|
|
@ -2,21 +2,14 @@ package lz4
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/bits"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidSourceShortBuffer is returned by UncompressBlock or CompressBLock when a compressed
|
||||
// block is corrupted or the destination buffer is not large enough for the uncompressed data.
|
||||
ErrInvalidSourceShortBuffer = errors.New("lz4: invalid source or destination buffer too short")
|
||||
// ErrInvalid is returned when reading an invalid LZ4 archive.
|
||||
ErrInvalid = errors.New("lz4: bad magic number")
|
||||
)
|
||||
|
||||
// blockHash hashes 4 bytes into a value < winSize.
|
||||
func blockHash(x uint32) uint32 {
|
||||
const hasher uint32 = 2654435761 // Knuth multiplicative hash.
|
||||
return x * hasher >> hashShift
|
||||
// blockHash hashes the lower 6 bytes into a value < htSize.
|
||||
func blockHash(x uint64) uint32 {
|
||||
const prime6bytes = 227718039650203
|
||||
return uint32(((x << (64 - 48)) * prime6bytes) >> (64 - hashLog))
|
||||
}
|
||||
|
||||
// CompressBlockBound returns the maximum size of a given buffer of size n, when not compressible.
|
||||
|
@ -30,137 +23,127 @@ func CompressBlockBound(n int) int {
|
|||
// The destination buffer must be sized appropriately.
|
||||
//
|
||||
// An error is returned if the source data is invalid or the destination buffer is too small.
|
||||
func UncompressBlock(src, dst []byte) (si int, err error) {
|
||||
defer func() {
|
||||
// It is now faster to let the runtime panic and recover on out of bound slice access
|
||||
// than checking indices as we go along.
|
||||
if recover() != nil {
|
||||
err = ErrInvalidSourceShortBuffer
|
||||
}
|
||||
}()
|
||||
sn := len(src)
|
||||
if sn == 0 {
|
||||
func UncompressBlock(src, dst []byte) (int, error) {
|
||||
if len(src) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var di int
|
||||
|
||||
for {
|
||||
// Literals and match lengths (token).
|
||||
b := int(src[si])
|
||||
si++
|
||||
|
||||
// Literals.
|
||||
if lLen := b >> 4; lLen > 0 {
|
||||
if lLen == 0xF {
|
||||
for src[si] == 0xFF {
|
||||
lLen += 0xFF
|
||||
si++
|
||||
}
|
||||
lLen += int(src[si])
|
||||
si++
|
||||
}
|
||||
i := si
|
||||
si += lLen
|
||||
di += copy(dst[di:], src[i:si])
|
||||
|
||||
if si >= sn {
|
||||
return di, nil
|
||||
}
|
||||
}
|
||||
|
||||
si++
|
||||
_ = src[si] // Bound check elimination.
|
||||
offset := int(src[si-1]) | int(src[si])<<8
|
||||
si++
|
||||
|
||||
// Match.
|
||||
mLen := b & 0xF
|
||||
if mLen == 0xF {
|
||||
for src[si] == 0xFF {
|
||||
mLen += 0xFF
|
||||
si++
|
||||
}
|
||||
mLen += int(src[si])
|
||||
si++
|
||||
}
|
||||
mLen += minMatch
|
||||
|
||||
// Copy the match.
|
||||
i := di - offset
|
||||
if offset > 0 && mLen >= offset {
|
||||
// Efficiently copy the match dst[di-offset:di] into the dst slice.
|
||||
bytesToCopy := offset * (mLen / offset)
|
||||
expanded := dst[i:]
|
||||
for n := offset; n <= bytesToCopy+offset; n *= 2 {
|
||||
copy(expanded[n:], expanded[:n])
|
||||
}
|
||||
di += bytesToCopy
|
||||
mLen -= bytesToCopy
|
||||
}
|
||||
di += copy(dst[di:], dst[i:i+mLen])
|
||||
if di := decodeBlock(dst, src); di >= 0 {
|
||||
return di, nil
|
||||
}
|
||||
return 0, ErrInvalidSourceShortBuffer
|
||||
}
|
||||
|
||||
// CompressBlock compresses the source buffer into the destination one.
|
||||
// This is the fast version of LZ4 compression and also the default one.
|
||||
// The size of hashTable must be at least 64Kb.
|
||||
//
|
||||
// The size of the compressed data is returned. If it is 0 and no error, then the data is incompressible.
|
||||
// The argument hashTable is scratch space for a hash table used by the
|
||||
// compressor. If provided, it should have length at least 1<<16. If it is
|
||||
// shorter (or nil), CompressBlock allocates its own hash table.
|
||||
//
|
||||
// The size of the compressed data is returned.
|
||||
//
|
||||
// If the destination buffer size is lower than CompressBlockBound and
|
||||
// the compressed size is 0 and no error, then the data is incompressible.
|
||||
//
|
||||
// An error is returned if the destination buffer is too small.
|
||||
func CompressBlock(src, dst []byte, hashTable []int) (di int, err error) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
err = ErrInvalidSourceShortBuffer
|
||||
}
|
||||
}()
|
||||
func CompressBlock(src, dst []byte, hashTable []int) (_ int, err error) {
|
||||
defer recoverBlock(&err)
|
||||
|
||||
sn, dn := len(src)-mfLimit, len(dst)
|
||||
if sn <= 0 || dn == 0 {
|
||||
return 0, nil
|
||||
// Return 0, nil only if the destination buffer size is < CompressBlockBound.
|
||||
isNotCompressible := len(dst) < CompressBlockBound(len(src))
|
||||
|
||||
// adaptSkipLog sets how quickly the compressor begins skipping blocks when data is incompressible.
|
||||
// This significantly speeds up incompressible data and usually has very small impact on compression.
|
||||
// bytes to skip = 1 + (bytes since last match >> adaptSkipLog)
|
||||
const adaptSkipLog = 7
|
||||
if len(hashTable) < htSize {
|
||||
htIface := htPool.Get()
|
||||
defer htPool.Put(htIface)
|
||||
hashTable = (*(htIface).(*[htSize]int))[:]
|
||||
}
|
||||
// Prove to the compiler the table has at least htSize elements.
|
||||
// The compiler can see that "uint32() >> hashShift" cannot be out of bounds.
|
||||
hashTable = hashTable[:htSize]
|
||||
|
||||
// si: Current position of the search.
|
||||
// anchor: Position of the current literals.
|
||||
var si, di, anchor int
|
||||
sn := len(src) - mfLimit
|
||||
if sn <= 0 {
|
||||
goto lastLiterals
|
||||
}
|
||||
var si int
|
||||
|
||||
// Fast scan strategy: the hash table only stores the last 4 bytes sequences.
|
||||
// const accInit = 1 << skipStrength
|
||||
|
||||
anchor := si // Position of the current literals.
|
||||
// acc := accInit // Variable step: improves performance on non-compressible data.
|
||||
|
||||
for si < sn {
|
||||
// Hash the next 4 bytes (sequence)...
|
||||
match := binary.LittleEndian.Uint32(src[si:])
|
||||
// Hash the next 6 bytes (sequence)...
|
||||
match := binary.LittleEndian.Uint64(src[si:])
|
||||
h := blockHash(match)
|
||||
h2 := blockHash(match >> 8)
|
||||
|
||||
// We check a match at s, s+1 and s+2 and pick the first one we get.
|
||||
// Checking 3 only requires us to load the source one.
|
||||
ref := hashTable[h]
|
||||
ref2 := hashTable[h2]
|
||||
hashTable[h] = si
|
||||
if ref >= sn { // Invalid reference (dirty hashtable).
|
||||
si++
|
||||
continue
|
||||
}
|
||||
hashTable[h2] = si + 1
|
||||
offset := si - ref
|
||||
|
||||
// If offset <= 0 we got an old entry in the hash table.
|
||||
if offset <= 0 || offset >= winSize || // Out of window.
|
||||
match != binary.LittleEndian.Uint32(src[ref:]) { // Hash collision on different matches.
|
||||
// si += acc >> skipStrength
|
||||
// acc++
|
||||
si++
|
||||
continue
|
||||
uint32(match) != binary.LittleEndian.Uint32(src[ref:]) { // Hash collision on different matches.
|
||||
// No match. Start calculating another hash.
|
||||
// The processor can usually do this out-of-order.
|
||||
h = blockHash(match >> 16)
|
||||
ref = hashTable[h]
|
||||
|
||||
// Check the second match at si+1
|
||||
si += 1
|
||||
offset = si - ref2
|
||||
|
||||
if offset <= 0 || offset >= winSize ||
|
||||
uint32(match>>8) != binary.LittleEndian.Uint32(src[ref2:]) {
|
||||
// No match. Check the third match at si+2
|
||||
si += 1
|
||||
offset = si - ref
|
||||
hashTable[h] = si
|
||||
|
||||
if offset <= 0 || offset >= winSize ||
|
||||
uint32(match>>16) != binary.LittleEndian.Uint32(src[ref:]) {
|
||||
// Skip one extra byte (at si+3) before we check 3 matches again.
|
||||
si += 2 + (si-anchor)>>adaptSkipLog
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match found.
|
||||
// acc = accInit
|
||||
lLen := si - anchor // Literal length.
|
||||
// We already matched 4 bytes.
|
||||
mLen := 4
|
||||
|
||||
// Encode match length part 1.
|
||||
si += minMatch
|
||||
mLen := si // Match length has minMatch already.
|
||||
// Find the longest match, first looking by batches of 8 bytes.
|
||||
for si < sn && binary.LittleEndian.Uint64(src[si:]) == binary.LittleEndian.Uint64(src[si-offset:]) {
|
||||
si += 8
|
||||
// Extend backwards if we can, reducing literals.
|
||||
tOff := si - offset - 1
|
||||
for lLen > 0 && tOff >= 0 && src[si-1] == src[tOff] {
|
||||
si--
|
||||
tOff--
|
||||
lLen--
|
||||
mLen++
|
||||
}
|
||||
// Then byte by byte.
|
||||
for si < sn && src[si] == src[si-offset] {
|
||||
si++
|
||||
|
||||
// Add the match length, so we continue search at the end.
|
||||
// Use mLen to store the offset base.
|
||||
si, mLen = si+mLen, si+minMatch
|
||||
|
||||
// Find the longest match by looking by batches of 8 bytes.
|
||||
for si+8 < sn {
|
||||
x := binary.LittleEndian.Uint64(src[si:]) ^ binary.LittleEndian.Uint64(src[si-offset:])
|
||||
if x == 0 {
|
||||
si += 8
|
||||
} else {
|
||||
// Stop is first non-zero byte.
|
||||
si += bits.TrailingZeros64(x) >> 3
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mLen = si - mLen
|
||||
|
@ -186,7 +169,7 @@ func CompressBlock(src, dst []byte, hashTable []int) (di int, err error) {
|
|||
di++
|
||||
|
||||
// Literals.
|
||||
copy(dst[di:], src[anchor:anchor+lLen])
|
||||
copy(dst[di:di+lLen], src[anchor:anchor+lLen])
|
||||
di += lLen + 2
|
||||
anchor = si
|
||||
|
||||
|
@ -203,9 +186,17 @@ func CompressBlock(src, dst []byte, hashTable []int) (di int, err error) {
|
|||
dst[di] = byte(mLen)
|
||||
di++
|
||||
}
|
||||
// Check if we can load next values.
|
||||
if si >= sn {
|
||||
break
|
||||
}
|
||||
// Hash match end-2
|
||||
h = blockHash(binary.LittleEndian.Uint64(src[si-2:]))
|
||||
hashTable[h] = si - 2
|
||||
}
|
||||
|
||||
if anchor == 0 {
|
||||
lastLiterals:
|
||||
if isNotCompressible && anchor == 0 {
|
||||
// Incompressible.
|
||||
return 0, nil
|
||||
}
|
||||
|
@ -226,48 +217,68 @@ func CompressBlock(src, dst []byte, hashTable []int) (di int, err error) {
|
|||
di++
|
||||
|
||||
// Write the last literals.
|
||||
if di >= anchor {
|
||||
if isNotCompressible && di >= anchor {
|
||||
// Incompressible.
|
||||
return 0, nil
|
||||
}
|
||||
di += copy(dst[di:], src[anchor:])
|
||||
di += copy(dst[di:di+len(src)-anchor], src[anchor:])
|
||||
return di, nil
|
||||
}
|
||||
|
||||
// Pool of hash tables for CompressBlock.
|
||||
var htPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new([htSize]int)
|
||||
},
|
||||
}
|
||||
|
||||
// blockHash hashes 4 bytes into a value < winSize.
|
||||
func blockHashHC(x uint32) uint32 {
|
||||
const hasher uint32 = 2654435761 // Knuth multiplicative hash.
|
||||
return x * hasher >> (32 - winSizeLog)
|
||||
}
|
||||
|
||||
// CompressBlockHC compresses the source buffer src into the destination dst
|
||||
// with max search depth (use 0 or negative value for no max).
|
||||
//
|
||||
// CompressBlockHC compression ratio is better than CompressBlock but it is also slower.
|
||||
//
|
||||
// The size of the compressed data is returned. If it is 0 and no error, then the data is not compressible.
|
||||
// The size of the compressed data is returned.
|
||||
//
|
||||
// If the destination buffer size is lower than CompressBlockBound and
|
||||
// the compressed size is 0 and no error, then the data is incompressible.
|
||||
//
|
||||
// An error is returned if the destination buffer is too small.
|
||||
func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
err = ErrInvalidSourceShortBuffer
|
||||
}
|
||||
}()
|
||||
func CompressBlockHC(src, dst []byte, depth int) (_ int, err error) {
|
||||
defer recoverBlock(&err)
|
||||
|
||||
sn, dn := len(src)-mfLimit, len(dst)
|
||||
if sn <= 0 || dn == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var si int
|
||||
// Return 0, nil only if the destination buffer size is < CompressBlockBound.
|
||||
isNotCompressible := len(dst) < CompressBlockBound(len(src))
|
||||
|
||||
// adaptSkipLog sets how quickly the compressor begins skipping blocks when data is incompressible.
|
||||
// This significantly speeds up incompressible data and usually has very small impact on compression.
|
||||
// bytes to skip = 1 + (bytes since last match >> adaptSkipLog)
|
||||
const adaptSkipLog = 7
|
||||
|
||||
var si, di, anchor int
|
||||
|
||||
// hashTable: stores the last position found for a given hash
|
||||
// chaingTable: stores previous positions for a given hash
|
||||
// chainTable: stores previous positions for a given hash
|
||||
var hashTable, chainTable [winSize]int
|
||||
|
||||
if depth <= 0 {
|
||||
depth = winSize
|
||||
}
|
||||
|
||||
anchor := si
|
||||
sn := len(src) - mfLimit
|
||||
if sn <= 0 {
|
||||
goto lastLiterals
|
||||
}
|
||||
|
||||
for si < sn {
|
||||
// Hash the next 4 bytes (sequence).
|
||||
match := binary.LittleEndian.Uint32(src[si:])
|
||||
h := blockHash(match)
|
||||
h := blockHashHC(match)
|
||||
|
||||
// Follow the chain until out of window and give the longest match.
|
||||
mLen := 0
|
||||
|
@ -280,13 +291,17 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
}
|
||||
ml := 0
|
||||
// Compare the current position with a previous with the same hash.
|
||||
for ml < sn-si && binary.LittleEndian.Uint64(src[next+ml:]) == binary.LittleEndian.Uint64(src[si+ml:]) {
|
||||
ml += 8
|
||||
for ml < sn-si {
|
||||
x := binary.LittleEndian.Uint64(src[next+ml:]) ^ binary.LittleEndian.Uint64(src[si+ml:])
|
||||
if x == 0 {
|
||||
ml += 8
|
||||
} else {
|
||||
// Stop is first non-zero byte.
|
||||
ml += bits.TrailingZeros64(x) >> 3
|
||||
break
|
||||
}
|
||||
}
|
||||
for ml < sn-si && src[next+ml] == src[si+ml] {
|
||||
ml++
|
||||
}
|
||||
if ml+1 < minMatch || ml <= mLen {
|
||||
if ml < minMatch || ml <= mLen {
|
||||
// Match too small (<minMath) or smaller than the current match.
|
||||
continue
|
||||
}
|
||||
|
@ -301,7 +316,7 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
|
||||
// No match found.
|
||||
if mLen == 0 {
|
||||
si++
|
||||
si += 1 + (si-anchor)>>adaptSkipLog
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -315,7 +330,7 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
for si, ml := winStart, si+mLen; si < ml; {
|
||||
match >>= 8
|
||||
match |= uint32(src[si+3]) << 24
|
||||
h := blockHash(match)
|
||||
h := blockHashHC(match)
|
||||
chainTable[si&winMask] = hashTable[h]
|
||||
hashTable[h] = si
|
||||
si++
|
||||
|
@ -347,7 +362,7 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
di++
|
||||
|
||||
// Literals.
|
||||
copy(dst[di:], src[anchor:anchor+lLen])
|
||||
copy(dst[di:di+lLen], src[anchor:anchor+lLen])
|
||||
di += lLen
|
||||
anchor = si
|
||||
|
||||
|
@ -366,12 +381,13 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if anchor == 0 {
|
||||
if isNotCompressible && anchor == 0 {
|
||||
// Incompressible.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Last literals.
|
||||
lastLiterals:
|
||||
lLen := len(src) - anchor
|
||||
if lLen < 0xF {
|
||||
dst[di] = byte(lLen << 4)
|
||||
|
@ -388,10 +404,10 @@ func CompressBlockHC(src, dst []byte, depth int) (di int, err error) {
|
|||
di++
|
||||
|
||||
// Write the last literals.
|
||||
if di >= anchor {
|
||||
if isNotCompressible && di >= anchor {
|
||||
// Incompressible.
|
||||
return 0, nil
|
||||
}
|
||||
di += copy(dst[di:], src[anchor:])
|
||||
di += copy(dst[di:di+len(src)-anchor], src[anchor:])
|
||||
return di, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// +build !appengine
|
||||
// +build gc
|
||||
// +build !noasm
|
||||
|
||||
package lz4
|
||||
|
||||
//go:noescape
|
||||
func decodeBlock(dst, src []byte) int
|
|
@ -0,0 +1,375 @@
|
|||
// +build !appengine
|
||||
// +build gc
|
||||
// +build !noasm
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// AX scratch
|
||||
// BX scratch
|
||||
// CX scratch
|
||||
// DX token
|
||||
//
|
||||
// DI &dst
|
||||
// SI &src
|
||||
// R8 &dst + len(dst)
|
||||
// R9 &src + len(src)
|
||||
// R11 &dst
|
||||
// R12 short output end
|
||||
// R13 short input end
|
||||
// func decodeBlock(dst, src []byte) int
|
||||
// using 50 bytes of stack currently
|
||||
TEXT ·decodeBlock(SB), NOSPLIT, $64-56
|
||||
MOVQ dst_base+0(FP), DI
|
||||
MOVQ DI, R11
|
||||
MOVQ dst_len+8(FP), R8
|
||||
ADDQ DI, R8
|
||||
|
||||
MOVQ src_base+24(FP), SI
|
||||
MOVQ src_len+32(FP), R9
|
||||
ADDQ SI, R9
|
||||
|
||||
// shortcut ends
|
||||
// short output end
|
||||
MOVQ R8, R12
|
||||
SUBQ $32, R12
|
||||
// short input end
|
||||
MOVQ R9, R13
|
||||
SUBQ $16, R13
|
||||
|
||||
loop:
|
||||
// for si < len(src)
|
||||
CMPQ SI, R9
|
||||
JGE end
|
||||
|
||||
// token := uint32(src[si])
|
||||
MOVBQZX (SI), DX
|
||||
INCQ SI
|
||||
|
||||
// lit_len = token >> 4
|
||||
// if lit_len > 0
|
||||
// CX = lit_len
|
||||
MOVQ DX, CX
|
||||
SHRQ $4, CX
|
||||
|
||||
// if lit_len != 0xF
|
||||
CMPQ CX, $0xF
|
||||
JEQ lit_len_loop_pre
|
||||
CMPQ DI, R12
|
||||
JGE lit_len_loop_pre
|
||||
CMPQ SI, R13
|
||||
JGE lit_len_loop_pre
|
||||
|
||||
// copy shortcut
|
||||
|
||||
// A two-stage shortcut for the most common case:
|
||||
// 1) If the literal length is 0..14, and there is enough space,
|
||||
// enter the shortcut and copy 16 bytes on behalf of the literals
|
||||
// (in the fast mode, only 8 bytes can be safely copied this way).
|
||||
// 2) Further if the match length is 4..18, copy 18 bytes in a similar
|
||||
// manner; but we ensure that there's enough space in the output for
|
||||
// those 18 bytes earlier, upon entering the shortcut (in other words,
|
||||
// there is a combined check for both stages).
|
||||
|
||||
// copy literal
|
||||
MOVOU (SI), X0
|
||||
MOVOU X0, (DI)
|
||||
ADDQ CX, DI
|
||||
ADDQ CX, SI
|
||||
|
||||
MOVQ DX, CX
|
||||
ANDQ $0xF, CX
|
||||
|
||||
// The second stage: prepare for match copying, decode full info.
|
||||
// If it doesn't work out, the info won't be wasted.
|
||||
// offset := uint16(data[:2])
|
||||
MOVWQZX (SI), DX
|
||||
ADDQ $2, SI
|
||||
|
||||
MOVQ DI, AX
|
||||
SUBQ DX, AX
|
||||
CMPQ AX, DI
|
||||
JGT err_short_buf
|
||||
|
||||
// if we can't do the second stage then jump straight to read the
|
||||
// match length, we already have the offset.
|
||||
CMPQ CX, $0xF
|
||||
JEQ match_len_loop_pre
|
||||
CMPQ DX, $8
|
||||
JLT match_len_loop_pre
|
||||
CMPQ AX, R11
|
||||
JLT err_short_buf
|
||||
|
||||
// memcpy(op + 0, match + 0, 8);
|
||||
MOVQ (AX), BX
|
||||
MOVQ BX, (DI)
|
||||
// memcpy(op + 8, match + 8, 8);
|
||||
MOVQ 8(AX), BX
|
||||
MOVQ BX, 8(DI)
|
||||
// memcpy(op +16, match +16, 2);
|
||||
MOVW 16(AX), BX
|
||||
MOVW BX, 16(DI)
|
||||
|
||||
ADDQ $4, DI // minmatch
|
||||
ADDQ CX, DI
|
||||
|
||||
// shortcut complete, load next token
|
||||
JMP loop
|
||||
|
||||
lit_len_loop_pre:
|
||||
// if lit_len > 0
|
||||
CMPQ CX, $0
|
||||
JEQ offset
|
||||
CMPQ CX, $0xF
|
||||
JNE copy_literal
|
||||
|
||||
lit_len_loop:
|
||||
// for src[si] == 0xFF
|
||||
CMPB (SI), $0xFF
|
||||
JNE lit_len_finalise
|
||||
|
||||
// bounds check src[si+1]
|
||||
MOVQ SI, AX
|
||||
ADDQ $1, AX
|
||||
CMPQ AX, R9
|
||||
JGT err_short_buf
|
||||
|
||||
// lit_len += 0xFF
|
||||
ADDQ $0xFF, CX
|
||||
INCQ SI
|
||||
JMP lit_len_loop
|
||||
|
||||
lit_len_finalise:
|
||||
// lit_len += int(src[si])
|
||||
// si++
|
||||
MOVBQZX (SI), AX
|
||||
ADDQ AX, CX
|
||||
INCQ SI
|
||||
|
||||
copy_literal:
|
||||
// bounds check src and dst
|
||||
MOVQ SI, AX
|
||||
ADDQ CX, AX
|
||||
CMPQ AX, R9
|
||||
JGT err_short_buf
|
||||
|
||||
MOVQ DI, AX
|
||||
ADDQ CX, AX
|
||||
CMPQ AX, R8
|
||||
JGT err_short_buf
|
||||
|
||||
// whats a good cut off to call memmove?
|
||||
CMPQ CX, $16
|
||||
JGT memmove_lit
|
||||
|
||||
// if len(dst[di:]) < 16
|
||||
MOVQ R8, AX
|
||||
SUBQ DI, AX
|
||||
CMPQ AX, $16
|
||||
JLT memmove_lit
|
||||
|
||||
// if len(src[si:]) < 16
|
||||
MOVQ R9, AX
|
||||
SUBQ SI, AX
|
||||
CMPQ AX, $16
|
||||
JLT memmove_lit
|
||||
|
||||
MOVOU (SI), X0
|
||||
MOVOU X0, (DI)
|
||||
|
||||
JMP finish_lit_copy
|
||||
|
||||
memmove_lit:
|
||||
// memmove(to, from, len)
|
||||
MOVQ DI, 0(SP)
|
||||
MOVQ SI, 8(SP)
|
||||
MOVQ CX, 16(SP)
|
||||
// spill
|
||||
MOVQ DI, 24(SP)
|
||||
MOVQ SI, 32(SP)
|
||||
MOVQ CX, 40(SP) // need len to inc SI, DI after
|
||||
MOVB DX, 48(SP)
|
||||
CALL runtime·memmove(SB)
|
||||
|
||||
// restore registers
|
||||
MOVQ 24(SP), DI
|
||||
MOVQ 32(SP), SI
|
||||
MOVQ 40(SP), CX
|
||||
MOVB 48(SP), DX
|
||||
|
||||
// recalc initial values
|
||||
MOVQ dst_base+0(FP), R8
|
||||
MOVQ R8, R11
|
||||
ADDQ dst_len+8(FP), R8
|
||||
MOVQ src_base+24(FP), R9
|
||||
ADDQ src_len+32(FP), R9
|
||||
MOVQ R8, R12
|
||||
SUBQ $32, R12
|
||||
MOVQ R9, R13
|
||||
SUBQ $16, R13
|
||||
|
||||
finish_lit_copy:
|
||||
ADDQ CX, SI
|
||||
ADDQ CX, DI
|
||||
|
||||
CMPQ SI, R9
|
||||
JGE end
|
||||
|
||||
offset:
|
||||
// CX := mLen
|
||||
// free up DX to use for offset
|
||||
MOVQ DX, CX
|
||||
|
||||
MOVQ SI, AX
|
||||
ADDQ $2, AX
|
||||
CMPQ AX, R9
|
||||
JGT err_short_buf
|
||||
|
||||
// offset
|
||||
// DX := int(src[si]) | int(src[si+1])<<8
|
||||
MOVWQZX (SI), DX
|
||||
ADDQ $2, SI
|
||||
|
||||
// 0 offset is invalid
|
||||
CMPQ DX, $0
|
||||
JEQ err_corrupt
|
||||
|
||||
ANDB $0xF, CX
|
||||
|
||||
match_len_loop_pre:
|
||||
// if mlen != 0xF
|
||||
CMPB CX, $0xF
|
||||
JNE copy_match
|
||||
|
||||
match_len_loop:
|
||||
// for src[si] == 0xFF
|
||||
// lit_len += 0xFF
|
||||
CMPB (SI), $0xFF
|
||||
JNE match_len_finalise
|
||||
|
||||
// bounds check src[si+1]
|
||||
MOVQ SI, AX
|
||||
ADDQ $1, AX
|
||||
CMPQ AX, R9
|
||||
JGT err_short_buf
|
||||
|
||||
ADDQ $0xFF, CX
|
||||
INCQ SI
|
||||
JMP match_len_loop
|
||||
|
||||
match_len_finalise:
|
||||
// lit_len += int(src[si])
|
||||
// si++
|
||||
MOVBQZX (SI), AX
|
||||
ADDQ AX, CX
|
||||
INCQ SI
|
||||
|
||||
copy_match:
|
||||
// mLen += minMatch
|
||||
ADDQ $4, CX
|
||||
|
||||
// check we have match_len bytes left in dst
|
||||
// di+match_len < len(dst)
|
||||
MOVQ DI, AX
|
||||
ADDQ CX, AX
|
||||
CMPQ AX, R8
|
||||
JGT err_short_buf
|
||||
|
||||
// DX = offset
|
||||
// CX = match_len
|
||||
// BX = &dst + (di - offset)
|
||||
MOVQ DI, BX
|
||||
SUBQ DX, BX
|
||||
|
||||
// check BX is within dst
|
||||
// if BX < &dst
|
||||
CMPQ BX, R11
|
||||
JLT err_short_buf
|
||||
|
||||
// if offset + match_len < di
|
||||
MOVQ BX, AX
|
||||
ADDQ CX, AX
|
||||
CMPQ DI, AX
|
||||
JGT copy_interior_match
|
||||
|
||||
// AX := len(dst[:di])
|
||||
// MOVQ DI, AX
|
||||
// SUBQ R11, AX
|
||||
|
||||
// copy 16 bytes at a time
|
||||
// if di-offset < 16 copy 16-(di-offset) bytes to di
|
||||
// then do the remaining
|
||||
|
||||
copy_match_loop:
|
||||
// for match_len >= 0
|
||||
// dst[di] = dst[i]
|
||||
// di++
|
||||
// i++
|
||||
MOVB (BX), AX
|
||||
MOVB AX, (DI)
|
||||
INCQ DI
|
||||
INCQ BX
|
||||
DECQ CX
|
||||
|
||||
CMPQ CX, $0
|
||||
JGT copy_match_loop
|
||||
|
||||
JMP loop
|
||||
|
||||
copy_interior_match:
|
||||
CMPQ CX, $16
|
||||
JGT memmove_match
|
||||
|
||||
// if len(dst[di:]) < 16
|
||||
MOVQ R8, AX
|
||||
SUBQ DI, AX
|
||||
CMPQ AX, $16
|
||||
JLT memmove_match
|
||||
|
||||
MOVOU (BX), X0
|
||||
MOVOU X0, (DI)
|
||||
|
||||
ADDQ CX, DI
|
||||
JMP loop
|
||||
|
||||
memmove_match:
|
||||
// memmove(to, from, len)
|
||||
MOVQ DI, 0(SP)
|
||||
MOVQ BX, 8(SP)
|
||||
MOVQ CX, 16(SP)
|
||||
// spill
|
||||
MOVQ DI, 24(SP)
|
||||
MOVQ SI, 32(SP)
|
||||
MOVQ CX, 40(SP) // need len to inc SI, DI after
|
||||
CALL runtime·memmove(SB)
|
||||
|
||||
// restore registers
|
||||
MOVQ 24(SP), DI
|
||||
MOVQ 32(SP), SI
|
||||
MOVQ 40(SP), CX
|
||||
|
||||
// recalc initial values
|
||||
MOVQ dst_base+0(FP), R8
|
||||
MOVQ R8, R11 // TODO: make these sensible numbers
|
||||
ADDQ dst_len+8(FP), R8
|
||||
MOVQ src_base+24(FP), R9
|
||||
ADDQ src_len+32(FP), R9
|
||||
MOVQ R8, R12
|
||||
SUBQ $32, R12
|
||||
MOVQ R9, R13
|
||||
SUBQ $16, R13
|
||||
|
||||
ADDQ CX, DI
|
||||
JMP loop
|
||||
|
||||
err_corrupt:
|
||||
MOVQ $-1, ret+48(FP)
|
||||
RET
|
||||
|
||||
err_short_buf:
|
||||
MOVQ $-2, ret+48(FP)
|
||||
RET
|
||||
|
||||
end:
|
||||
SUBQ R11, DI
|
||||
MOVQ DI, ret+48(FP)
|
||||
RET
|
|
@ -0,0 +1,98 @@
|
|||
// +build !amd64 appengine !gc noasm
|
||||
|
||||
package lz4
|
||||
|
||||
func decodeBlock(dst, src []byte) (ret int) {
|
||||
const hasError = -2
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
ret = hasError
|
||||
}
|
||||
}()
|
||||
|
||||
var si, di int
|
||||
for {
|
||||
// Literals and match lengths (token).
|
||||
b := int(src[si])
|
||||
si++
|
||||
|
||||
// Literals.
|
||||
if lLen := b >> 4; lLen > 0 {
|
||||
switch {
|
||||
case lLen < 0xF && si+16 < len(src):
|
||||
// Shortcut 1
|
||||
// if we have enough room in src and dst, and the literals length
|
||||
// is small enough (0..14) then copy all 16 bytes, even if not all
|
||||
// are part of the literals.
|
||||
copy(dst[di:], src[si:si+16])
|
||||
si += lLen
|
||||
di += lLen
|
||||
if mLen := b & 0xF; mLen < 0xF {
|
||||
// Shortcut 2
|
||||
// if the match length (4..18) fits within the literals, then copy
|
||||
// all 18 bytes, even if not all are part of the literals.
|
||||
mLen += 4
|
||||
if offset := int(src[si]) | int(src[si+1])<<8; mLen <= offset {
|
||||
i := di - offset
|
||||
end := i + 18
|
||||
if end > len(dst) {
|
||||
// The remaining buffer may not hold 18 bytes.
|
||||
// See https://github.com/pierrec/lz4/issues/51.
|
||||
end = len(dst)
|
||||
}
|
||||
copy(dst[di:], dst[i:end])
|
||||
si += 2
|
||||
di += mLen
|
||||
continue
|
||||
}
|
||||
}
|
||||
case lLen == 0xF:
|
||||
for src[si] == 0xFF {
|
||||
lLen += 0xFF
|
||||
si++
|
||||
}
|
||||
lLen += int(src[si])
|
||||
si++
|
||||
fallthrough
|
||||
default:
|
||||
copy(dst[di:di+lLen], src[si:si+lLen])
|
||||
si += lLen
|
||||
di += lLen
|
||||
}
|
||||
}
|
||||
if si >= len(src) {
|
||||
return di
|
||||
}
|
||||
|
||||
offset := int(src[si]) | int(src[si+1])<<8
|
||||
if offset == 0 {
|
||||
return hasError
|
||||
}
|
||||
si += 2
|
||||
|
||||
// Match.
|
||||
mLen := b & 0xF
|
||||
if mLen == 0xF {
|
||||
for src[si] == 0xFF {
|
||||
mLen += 0xFF
|
||||
si++
|
||||
}
|
||||
mLen += int(src[si])
|
||||
si++
|
||||
}
|
||||
mLen += minMatch
|
||||
|
||||
// Copy the match.
|
||||
expanded := dst[di-offset:]
|
||||
if mLen > offset {
|
||||
// Efficiently copy the match dst[di-offset:di] into the dst slice.
|
||||
bytesToCopy := offset * (mLen / offset)
|
||||
for n := offset; n <= bytesToCopy+offset; n *= 2 {
|
||||
copy(expanded[n:], expanded[:n])
|
||||
}
|
||||
di += bytesToCopy
|
||||
mLen -= bytesToCopy
|
||||
}
|
||||
di += copy(dst[di:di+mLen], expanded[:mLen])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package lz4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
rdebug "runtime/debug"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidSourceShortBuffer is returned by UncompressBlock or CompressBLock when a compressed
|
||||
// block is corrupted or the destination buffer is not large enough for the uncompressed data.
|
||||
ErrInvalidSourceShortBuffer = errors.New("lz4: invalid source or destination buffer too short")
|
||||
// ErrInvalid is returned when reading an invalid LZ4 archive.
|
||||
ErrInvalid = errors.New("lz4: bad magic number")
|
||||
// ErrBlockDependency is returned when attempting to decompress an archive created with block dependency.
|
||||
ErrBlockDependency = errors.New("lz4: block dependency not supported")
|
||||
// ErrUnsupportedSeek is returned when attempting to Seek any way but forward from the current position.
|
||||
ErrUnsupportedSeek = errors.New("lz4: can only seek forward from io.SeekCurrent")
|
||||
)
|
||||
|
||||
func recoverBlock(e *error) {
|
||||
if r := recover(); r != nil && *e == nil {
|
||||
if debugFlag {
|
||||
fmt.Fprintln(os.Stderr, r)
|
||||
rdebug.PrintStack()
|
||||
}
|
||||
*e = ErrInvalidSourceShortBuffer
|
||||
}
|
||||
}
|
|
@ -7,14 +7,15 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
prime32_1 uint32 = 2654435761
|
||||
prime32_2 uint32 = 2246822519
|
||||
prime32_3 uint32 = 3266489917
|
||||
prime32_4 uint32 = 668265263
|
||||
prime32_5 uint32 = 374761393
|
||||
prime1 uint32 = 2654435761
|
||||
prime2 uint32 = 2246822519
|
||||
prime3 uint32 = 3266489917
|
||||
prime4 uint32 = 668265263
|
||||
prime5 uint32 = 374761393
|
||||
|
||||
prime32_1plus2 uint32 = 606290984
|
||||
prime32_minus1 uint32 = 1640531535
|
||||
primeMask = 0xFFFFFFFF
|
||||
prime1plus2 = uint32((uint64(prime1) + uint64(prime2)) & primeMask) // 606290984
|
||||
prime1minus = uint32((-int64(prime1)) & primeMask) // 1640531535
|
||||
)
|
||||
|
||||
// XXHZero represents an xxhash32 object with seed 0.
|
||||
|
@ -37,10 +38,10 @@ func (xxh XXHZero) Sum(b []byte) []byte {
|
|||
|
||||
// Reset resets the Hash to its initial state.
|
||||
func (xxh *XXHZero) Reset() {
|
||||
xxh.v1 = prime32_1plus2
|
||||
xxh.v2 = prime32_2
|
||||
xxh.v1 = prime1plus2
|
||||
xxh.v2 = prime2
|
||||
xxh.v3 = 0
|
||||
xxh.v4 = prime32_minus1
|
||||
xxh.v4 = prime1minus
|
||||
xxh.totalLen = 0
|
||||
xxh.bufused = 0
|
||||
}
|
||||
|
@ -83,20 +84,20 @@ func (xxh *XXHZero) Write(input []byte) (int, error) {
|
|||
|
||||
// fast rotl(13)
|
||||
buf := xxh.buf[:16] // BCE hint.
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(buf[:])*prime32_2) * prime32_1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(buf[4:])*prime32_2) * prime32_1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(buf[8:])*prime32_2) * prime32_1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(buf[12:])*prime32_2) * prime32_1
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(buf[:])*prime2) * prime1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(buf[4:])*prime2) * prime1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(buf[8:])*prime2) * prime1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(buf[12:])*prime2) * prime1
|
||||
p = r
|
||||
xxh.bufused = 0
|
||||
}
|
||||
|
||||
for n := n - 16; p <= n; p += 16 {
|
||||
sub := input[p:][:16] //BCE hint for compiler
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(sub[:])*prime32_2) * prime32_1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(sub[4:])*prime32_2) * prime32_1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(sub[8:])*prime32_2) * prime32_1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(sub[12:])*prime32_2) * prime32_1
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(sub[:])*prime2) * prime1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(sub[4:])*prime2) * prime1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(sub[8:])*prime2) * prime1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(sub[12:])*prime2) * prime1
|
||||
}
|
||||
xxh.v1, xxh.v2, xxh.v3, xxh.v4 = v1, v2, v3, v4
|
||||
|
||||
|
@ -112,25 +113,25 @@ func (xxh *XXHZero) Sum32() uint32 {
|
|||
if h32 >= 16 {
|
||||
h32 += rol1(xxh.v1) + rol7(xxh.v2) + rol12(xxh.v3) + rol18(xxh.v4)
|
||||
} else {
|
||||
h32 += prime32_5
|
||||
h32 += prime5
|
||||
}
|
||||
|
||||
p := 0
|
||||
n := xxh.bufused
|
||||
buf := xxh.buf
|
||||
for n := n - 4; p <= n; p += 4 {
|
||||
h32 += binary.LittleEndian.Uint32(buf[p:p+4]) * prime32_3
|
||||
h32 = rol17(h32) * prime32_4
|
||||
h32 += binary.LittleEndian.Uint32(buf[p:p+4]) * prime3
|
||||
h32 = rol17(h32) * prime4
|
||||
}
|
||||
for ; p < n; p++ {
|
||||
h32 += uint32(buf[p]) * prime32_5
|
||||
h32 = rol11(h32) * prime32_1
|
||||
h32 += uint32(buf[p]) * prime5
|
||||
h32 = rol11(h32) * prime1
|
||||
}
|
||||
|
||||
h32 ^= h32 >> 15
|
||||
h32 *= prime32_2
|
||||
h32 *= prime2
|
||||
h32 ^= h32 >> 13
|
||||
h32 *= prime32_3
|
||||
h32 *= prime3
|
||||
h32 ^= h32 >> 16
|
||||
|
||||
return h32
|
||||
|
@ -142,19 +143,19 @@ func ChecksumZero(input []byte) uint32 {
|
|||
h32 := uint32(n)
|
||||
|
||||
if n < 16 {
|
||||
h32 += prime32_5
|
||||
h32 += prime5
|
||||
} else {
|
||||
v1 := prime32_1plus2
|
||||
v2 := prime32_2
|
||||
v1 := prime1plus2
|
||||
v2 := prime2
|
||||
v3 := uint32(0)
|
||||
v4 := prime32_minus1
|
||||
v4 := prime1minus
|
||||
p := 0
|
||||
for n := n - 16; p <= n; p += 16 {
|
||||
sub := input[p:][:16] //BCE hint for compiler
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(sub[:])*prime32_2) * prime32_1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(sub[4:])*prime32_2) * prime32_1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(sub[8:])*prime32_2) * prime32_1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(sub[12:])*prime32_2) * prime32_1
|
||||
v1 = rol13(v1+binary.LittleEndian.Uint32(sub[:])*prime2) * prime1
|
||||
v2 = rol13(v2+binary.LittleEndian.Uint32(sub[4:])*prime2) * prime1
|
||||
v3 = rol13(v3+binary.LittleEndian.Uint32(sub[8:])*prime2) * prime1
|
||||
v4 = rol13(v4+binary.LittleEndian.Uint32(sub[12:])*prime2) * prime1
|
||||
}
|
||||
input = input[p:]
|
||||
n -= p
|
||||
|
@ -163,19 +164,19 @@ func ChecksumZero(input []byte) uint32 {
|
|||
|
||||
p := 0
|
||||
for n := n - 4; p <= n; p += 4 {
|
||||
h32 += binary.LittleEndian.Uint32(input[p:p+4]) * prime32_3
|
||||
h32 = rol17(h32) * prime32_4
|
||||
h32 += binary.LittleEndian.Uint32(input[p:p+4]) * prime3
|
||||
h32 = rol17(h32) * prime4
|
||||
}
|
||||
for p < n {
|
||||
h32 += uint32(input[p]) * prime32_5
|
||||
h32 = rol11(h32) * prime32_1
|
||||
h32 += uint32(input[p]) * prime5
|
||||
h32 = rol11(h32) * prime1
|
||||
p++
|
||||
}
|
||||
|
||||
h32 ^= h32 >> 15
|
||||
h32 *= prime32_2
|
||||
h32 *= prime2
|
||||
h32 ^= h32 >> 13
|
||||
h32 *= prime32_3
|
||||
h32 *= prime3
|
||||
h32 ^= h32 >> 16
|
||||
|
||||
return h32
|
||||
|
@ -183,12 +184,12 @@ func ChecksumZero(input []byte) uint32 {
|
|||
|
||||
// Uint32Zero hashes x with seed 0.
|
||||
func Uint32Zero(x uint32) uint32 {
|
||||
h := prime32_5 + 4 + x*prime32_3
|
||||
h = rol17(h) * prime32_4
|
||||
h := prime5 + 4 + x*prime3
|
||||
h = rol17(h) * prime4
|
||||
h ^= h >> 15
|
||||
h *= prime32_2
|
||||
h *= prime2
|
||||
h ^= h >> 13
|
||||
h *= prime32_3
|
||||
h *= prime3
|
||||
h ^= h >> 16
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
//
|
||||
package lz4
|
||||
|
||||
import "math/bits"
|
||||
|
||||
import "sync"
|
||||
|
||||
const (
|
||||
// Extension is the LZ4 frame file name extension
|
||||
Extension = ".lz4"
|
||||
|
@ -30,26 +34,63 @@ const (
|
|||
// hashLog determines the size of the hash table used to quickly find a previous match position.
|
||||
// Its value influences the compression speed and memory usage, the lower the faster,
|
||||
// but at the expense of the compression ratio.
|
||||
// 16 seems to be the best compromise.
|
||||
hashLog = 16
|
||||
hashTableSize = 1 << hashLog
|
||||
hashShift = uint((minMatch * 8) - hashLog)
|
||||
// 16 seems to be the best compromise for fast compression.
|
||||
hashLog = 16
|
||||
htSize = 1 << hashLog
|
||||
|
||||
mfLimit = 8 + minMatch // The last match cannot start within the last 12 bytes.
|
||||
skipStrength = 6 // variable step for fast scan
|
||||
mfLimit = 10 + minMatch // The last match cannot start within the last 14 bytes.
|
||||
)
|
||||
|
||||
// map the block max size id with its value in bytes: 64Kb, 256Kb, 1Mb and 4Mb.
|
||||
var (
|
||||
bsMapID = map[byte]int{4: 64 << 10, 5: 256 << 10, 6: 1 << 20, 7: 4 << 20}
|
||||
bsMapValue = make(map[int]byte, len(bsMapID))
|
||||
const (
|
||||
blockSize64K = 1 << (16 + 2*iota)
|
||||
blockSize256K
|
||||
blockSize1M
|
||||
blockSize4M
|
||||
)
|
||||
|
||||
// Reversed.
|
||||
func init() {
|
||||
for i, v := range bsMapID {
|
||||
bsMapValue[v] = i
|
||||
var (
|
||||
// Keep a pool of buffers for each valid block sizes.
|
||||
bsMapValue = [...]*sync.Pool{
|
||||
newBufferPool(2 * blockSize64K),
|
||||
newBufferPool(2 * blockSize256K),
|
||||
newBufferPool(2 * blockSize1M),
|
||||
newBufferPool(2 * blockSize4M),
|
||||
}
|
||||
)
|
||||
|
||||
// newBufferPool returns a pool for buffers of the given size.
|
||||
func newBufferPool(size int) *sync.Pool {
|
||||
return &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, size)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getBuffer returns a buffer to its pool.
|
||||
func getBuffer(size int) []byte {
|
||||
idx := blockSizeValueToIndex(size) - 4
|
||||
return bsMapValue[idx].Get().([]byte)
|
||||
}
|
||||
|
||||
// putBuffer returns a buffer to its pool.
|
||||
func putBuffer(size int, buf []byte) {
|
||||
if cap(buf) > 0 {
|
||||
idx := blockSizeValueToIndex(size) - 4
|
||||
bsMapValue[idx].Put(buf[:cap(buf)])
|
||||
}
|
||||
}
|
||||
func blockSizeIndexToValue(i byte) int {
|
||||
return 1 << (16 + 2*uint(i))
|
||||
}
|
||||
func isValidBlockSize(size int) bool {
|
||||
const blockSizeMask = blockSize64K | blockSize256K | blockSize1M | blockSize4M
|
||||
|
||||
return size&blockSizeMask > 0 && bits.OnesCount(uint(size)) == 1
|
||||
}
|
||||
func blockSizeValueToIndex(size int) byte {
|
||||
return 4 + byte(bits.TrailingZeros(uint(size)>>16)/2)
|
||||
}
|
||||
|
||||
// Header describes the various flags that can be set on a Writer or obtained from a Reader.
|
||||
|
@ -57,7 +98,7 @@ func init() {
|
|||
// (http://fastcompression.blogspot.com/2013/04/lz4-streaming-format-final.html).
|
||||
//
|
||||
// NB. in a Reader, in case of concatenated frames, the Header values may change between Read() calls.
|
||||
// It is the caller responsibility to check them if necessary.
|
||||
// It is the caller's responsibility to check them if necessary.
|
||||
type Header struct {
|
||||
BlockChecksum bool // Compressed blocks checksum flag.
|
||||
NoChecksum bool // Frame checksum flag.
|
||||
|
@ -66,3 +107,7 @@ type Header struct {
|
|||
CompressionLevel int // Compression level (higher is better, use 0 for fastest compression).
|
||||
done bool // Header processed flag (Read or Write and checked).
|
||||
}
|
||||
|
||||
func (h *Header) Reset() {
|
||||
h.done = false
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import (
|
|||
// The Header may change between Read() calls in case of concatenated frames.
|
||||
type Reader struct {
|
||||
Header
|
||||
// Handler called when a block has been successfully read.
|
||||
// It provides the number of bytes read.
|
||||
OnBlockDone func(size int)
|
||||
|
||||
buf [8]byte // Scrap buffer.
|
||||
pos int64 // Current position in src.
|
||||
|
@ -22,6 +25,8 @@ type Reader struct {
|
|||
data []byte // Uncompressed data.
|
||||
idx int // Index of unread bytes into data.
|
||||
checksum xxh32.XXHZero // Frame hash.
|
||||
skip int64 // Bytes to skip before next read.
|
||||
dpos int64 // Position in dest
|
||||
}
|
||||
|
||||
// NewReader returns a new LZ4 frame decoder.
|
||||
|
@ -76,17 +81,17 @@ func (z *Reader) readHeader(first bool) error {
|
|||
return fmt.Errorf("lz4: invalid version: got %d; expected %d", v, Version)
|
||||
}
|
||||
if b>>5&1 == 0 {
|
||||
return fmt.Errorf("lz4: block dependency not supported")
|
||||
return ErrBlockDependency
|
||||
}
|
||||
z.BlockChecksum = b>>4&1 > 0
|
||||
frameSize := b>>3&1 > 0
|
||||
z.NoChecksum = b>>2&1 == 0
|
||||
|
||||
bmsID := buf[1] >> 4 & 0x7
|
||||
bSize, ok := bsMapID[bmsID]
|
||||
if !ok {
|
||||
if bmsID < 4 || bmsID > 7 {
|
||||
return fmt.Errorf("lz4: invalid block max size ID: %d", bmsID)
|
||||
}
|
||||
bSize := blockSizeIndexToValue(bmsID - 4)
|
||||
z.BlockMaxSize = bSize
|
||||
|
||||
// Allocate the compressed/uncompressed buffers.
|
||||
|
@ -101,7 +106,7 @@ func (z *Reader) readHeader(first bool) error {
|
|||
z.data = z.zdata[:cap(z.zdata)][bSize:]
|
||||
z.idx = len(z.data)
|
||||
|
||||
z.checksum.Write(buf[0:2])
|
||||
_, _ = z.checksum.Write(buf[0:2])
|
||||
|
||||
if frameSize {
|
||||
buf := buf[:8]
|
||||
|
@ -110,7 +115,7 @@ func (z *Reader) readHeader(first bool) error {
|
|||
}
|
||||
z.Size = binary.LittleEndian.Uint64(buf)
|
||||
z.pos += 8
|
||||
z.checksum.Write(buf)
|
||||
_, _ = z.checksum.Write(buf)
|
||||
}
|
||||
|
||||
// Header checksum.
|
||||
|
@ -158,6 +163,9 @@ func (z *Reader) Read(buf []byte) (int, error) {
|
|||
if debugFlag {
|
||||
debug("reading block from writer")
|
||||
}
|
||||
// Reset uncompressed buffer
|
||||
z.data = z.zdata[:cap(z.zdata)][len(z.zdata):]
|
||||
|
||||
// Block length: 0 = end of frame, highest bit set: uncompressed.
|
||||
bLen, err := z.readUint32()
|
||||
if err != nil {
|
||||
|
@ -208,6 +216,9 @@ func (z *Reader) Read(buf []byte) (int, error) {
|
|||
return 0, err
|
||||
}
|
||||
z.pos += int64(bLen)
|
||||
if z.OnBlockDone != nil {
|
||||
z.OnBlockDone(int(bLen))
|
||||
}
|
||||
|
||||
if z.BlockChecksum {
|
||||
checksum, err := z.readUint32()
|
||||
|
@ -252,10 +263,13 @@ func (z *Reader) Read(buf []byte) (int, error) {
|
|||
return 0, err
|
||||
}
|
||||
z.data = z.data[:n]
|
||||
if z.OnBlockDone != nil {
|
||||
z.OnBlockDone(n)
|
||||
}
|
||||
}
|
||||
|
||||
if !z.NoChecksum {
|
||||
z.checksum.Write(z.data)
|
||||
_, _ = z.checksum.Write(z.data)
|
||||
if debugFlag {
|
||||
debug("current frame checksum %x", z.checksum.Sum32())
|
||||
}
|
||||
|
@ -263,8 +277,20 @@ func (z *Reader) Read(buf []byte) (int, error) {
|
|||
z.idx = 0
|
||||
}
|
||||
|
||||
if z.skip > int64(len(z.data[z.idx:])) {
|
||||
z.skip -= int64(len(z.data[z.idx:]))
|
||||
z.dpos += int64(len(z.data[z.idx:]))
|
||||
z.idx = len(z.data)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
z.idx += int(z.skip)
|
||||
z.dpos += z.skip
|
||||
z.skip = 0
|
||||
|
||||
n := copy(buf, z.data[z.idx:])
|
||||
z.idx += n
|
||||
z.dpos += int64(n)
|
||||
if debugFlag {
|
||||
debug("copied %d bytes to input", n)
|
||||
}
|
||||
|
@ -272,6 +298,20 @@ func (z *Reader) Read(buf []byte) (int, error) {
|
|||
return n, nil
|
||||
}
|
||||
|
||||
// Seek implements io.Seeker, but supports seeking forward from the current
|
||||
// position only. Any other seek will return an error. Allows skipping output
|
||||
// bytes which aren't needed, which in some scenarios is faster than reading
|
||||
// and discarding them.
|
||||
// Note this may cause future calls to Read() to read 0 bytes if all of the
|
||||
// data they would have returned is skipped.
|
||||
func (z *Reader) Seek(offset int64, whence int) (int64, error) {
|
||||
if offset < 0 || whence != io.SeekCurrent {
|
||||
return z.dpos + z.skip, ErrUnsupportedSeek
|
||||
}
|
||||
z.skip += offset
|
||||
return z.dpos + z.skip, nil
|
||||
}
|
||||
|
||||
// Reset discards the Reader's state and makes it equivalent to the
|
||||
// result of its original state from NewReader, but reading from r instead.
|
||||
// This permits reusing a Reader rather than allocating a new one.
|
||||
|
|
|
@ -3,22 +3,35 @@ package lz4
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/pierrec/lz4/internal/xxh32"
|
||||
"io"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// zResult contains the results of compressing a block.
|
||||
type zResult struct {
|
||||
size uint32 // Block header
|
||||
data []byte // Compressed data
|
||||
checksum uint32 // Data checksum
|
||||
}
|
||||
|
||||
// Writer implements the LZ4 frame encoder.
|
||||
type Writer struct {
|
||||
Header
|
||||
// Handler called when a block has been successfully written out.
|
||||
// It provides the number of bytes written.
|
||||
OnBlockDone func(size int)
|
||||
|
||||
buf [19]byte // magic number(4) + header(flags(2)+[Size(8)+DictID(4)]+checksum(1)) does not exceed 19 bytes
|
||||
dst io.Writer // Destination.
|
||||
checksum xxh32.XXHZero // Frame checksum.
|
||||
zdata []byte // Compressed data.
|
||||
data []byte // Data to be compressed.
|
||||
data []byte // Data to be compressed + buffer for compressed data.
|
||||
idx int // Index into data.
|
||||
hashtable [winSize]int // Hash table used in CompressBlock().
|
||||
|
||||
// For concurrency.
|
||||
c chan chan zResult // Channel for block compression goroutines and writer goroutine.
|
||||
err error // Any error encountered while writing to the underlying destination.
|
||||
}
|
||||
|
||||
// NewWriter returns a new LZ4 frame encoder.
|
||||
|
@ -26,28 +39,92 @@ type Writer struct {
|
|||
// The supplied Header is checked at the first Write.
|
||||
// It is ok to change it before the first Write but then not until a Reset() is performed.
|
||||
func NewWriter(dst io.Writer) *Writer {
|
||||
return &Writer{dst: dst}
|
||||
z := new(Writer)
|
||||
z.Reset(dst)
|
||||
return z
|
||||
}
|
||||
|
||||
// WithConcurrency sets the number of concurrent go routines used for compression.
|
||||
// A negative value sets the concurrency to GOMAXPROCS.
|
||||
func (z *Writer) WithConcurrency(n int) *Writer {
|
||||
switch {
|
||||
case n == 0 || n == 1:
|
||||
z.c = nil
|
||||
return z
|
||||
case n < 0:
|
||||
n = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
z.c = make(chan chan zResult, n)
|
||||
// Writer goroutine managing concurrent block compression goroutines.
|
||||
go func() {
|
||||
// Process next block compression item.
|
||||
for c := range z.c {
|
||||
// Read the next compressed block result.
|
||||
// Waiting here ensures that the blocks are output in the order they were sent.
|
||||
// The incoming channel is always closed as it indicates to the caller that
|
||||
// the block has been processed.
|
||||
res := <-c
|
||||
n := len(res.data)
|
||||
if n == 0 {
|
||||
// Notify the block compression routine that we are done with its result.
|
||||
// This is used when a sentinel block is sent to terminate the compression.
|
||||
close(c)
|
||||
return
|
||||
}
|
||||
// Write the block.
|
||||
if err := z.writeUint32(res.size); err != nil && z.err == nil {
|
||||
z.err = err
|
||||
}
|
||||
if _, err := z.dst.Write(res.data); err != nil && z.err == nil {
|
||||
z.err = err
|
||||
}
|
||||
if z.BlockChecksum {
|
||||
if err := z.writeUint32(res.checksum); err != nil && z.err == nil {
|
||||
z.err = err
|
||||
}
|
||||
}
|
||||
if isCompressed := res.size&compressedBlockFlag == 0; isCompressed {
|
||||
// It is now safe to release the buffer as no longer in use by any goroutine.
|
||||
putBuffer(cap(res.data), res.data)
|
||||
}
|
||||
if h := z.OnBlockDone; h != nil {
|
||||
h(n)
|
||||
}
|
||||
close(c)
|
||||
}
|
||||
}()
|
||||
return z
|
||||
}
|
||||
|
||||
// newBuffers instantiates new buffers which size matches the one in Header.
|
||||
// The returned buffers are for decompression and compression respectively.
|
||||
func (z *Writer) newBuffers() {
|
||||
bSize := z.Header.BlockMaxSize
|
||||
buf := getBuffer(bSize)
|
||||
z.data = buf[:bSize] // Uncompressed buffer is the first half.
|
||||
}
|
||||
|
||||
// freeBuffers puts the writer's buffers back to the pool.
|
||||
func (z *Writer) freeBuffers() {
|
||||
// Put the buffer back into the pool, if any.
|
||||
putBuffer(z.Header.BlockMaxSize, z.data)
|
||||
z.data = nil
|
||||
}
|
||||
|
||||
// writeHeader builds and writes the header (magic+header) to the underlying io.Writer.
|
||||
func (z *Writer) writeHeader() error {
|
||||
// Default to 4Mb if BlockMaxSize is not set.
|
||||
if z.Header.BlockMaxSize == 0 {
|
||||
z.Header.BlockMaxSize = bsMapID[7]
|
||||
z.Header.BlockMaxSize = blockSize4M
|
||||
}
|
||||
// The only option that needs to be validated.
|
||||
bSize := z.Header.BlockMaxSize
|
||||
bSizeID, ok := bsMapValue[bSize]
|
||||
if !ok {
|
||||
if !isValidBlockSize(z.Header.BlockMaxSize) {
|
||||
return fmt.Errorf("lz4: invalid block max size: %d", bSize)
|
||||
}
|
||||
// Allocate the compressed/uncompressed buffers.
|
||||
// The compressed buffer cannot exceed the uncompressed one.
|
||||
if n := 2 * bSize; cap(z.zdata) < n {
|
||||
z.zdata = make([]byte, n, n)
|
||||
}
|
||||
z.zdata = z.zdata[:bSize]
|
||||
z.data = z.zdata[:cap(z.zdata)][bSize:]
|
||||
z.newBuffers()
|
||||
z.idx = 0
|
||||
|
||||
// Size is optional.
|
||||
|
@ -67,7 +144,7 @@ func (z *Writer) writeHeader() error {
|
|||
flg |= 1 << 2
|
||||
}
|
||||
buf[4] = flg
|
||||
buf[5] = bSizeID << 4
|
||||
buf[5] = blockSizeValueToIndex(z.Header.BlockMaxSize) << 4
|
||||
|
||||
// Current buffer size: magic(4) + flags(1) + block max size (1).
|
||||
n := 6
|
||||
|
@ -147,28 +224,34 @@ func (z *Writer) Write(buf []byte) (int, error) {
|
|||
// compressBlock compresses a block.
|
||||
func (z *Writer) compressBlock(data []byte) error {
|
||||
if !z.NoChecksum {
|
||||
z.checksum.Write(data)
|
||||
_, _ = z.checksum.Write(data)
|
||||
}
|
||||
|
||||
if z.c != nil {
|
||||
c := make(chan zResult)
|
||||
z.c <- c // Send now to guarantee order
|
||||
go writerCompressBlock(c, z.Header, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
zdata := z.data[z.Header.BlockMaxSize:cap(z.data)]
|
||||
// The compressed block size cannot exceed the input's.
|
||||
var zn int
|
||||
var err error
|
||||
|
||||
if level := z.Header.CompressionLevel; level != 0 {
|
||||
zn, err = CompressBlockHC(data, z.zdata, level)
|
||||
zn, _ = CompressBlockHC(data, zdata, level)
|
||||
} else {
|
||||
zn, err = CompressBlock(data, z.zdata, z.hashtable[:])
|
||||
zn, _ = CompressBlock(data, zdata, z.hashtable[:])
|
||||
}
|
||||
|
||||
var zdata []byte
|
||||
var bLen uint32
|
||||
if debugFlag {
|
||||
debug("block compression %d => %d", len(data), zn)
|
||||
}
|
||||
if err == nil && zn > 0 && zn < len(data) {
|
||||
if zn > 0 && zn < len(data) {
|
||||
// Compressible and compressed size smaller than uncompressed: ok!
|
||||
bLen = uint32(zn)
|
||||
zdata = z.zdata[:zn]
|
||||
zdata = zdata[:zn]
|
||||
} else {
|
||||
// Uncompressed block.
|
||||
bLen = uint32(len(data)) | compressedBlockFlag
|
||||
|
@ -182,24 +265,26 @@ func (z *Writer) compressBlock(data []byte) error {
|
|||
if err := z.writeUint32(bLen); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := z.dst.Write(zdata); err != nil {
|
||||
written, err := z.dst.Write(zdata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if h := z.OnBlockDone; h != nil {
|
||||
h(written)
|
||||
}
|
||||
|
||||
if z.BlockChecksum {
|
||||
checksum := xxh32.ChecksumZero(zdata)
|
||||
if !z.BlockChecksum {
|
||||
if debugFlag {
|
||||
debug("block checksum %x", checksum)
|
||||
}
|
||||
if err := z.writeUint32(checksum); err != nil {
|
||||
return err
|
||||
debug("current frame checksum %x", z.checksum.Sum32())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
checksum := xxh32.ChecksumZero(zdata)
|
||||
if debugFlag {
|
||||
debug("current frame checksum %x", z.checksum.Sum32())
|
||||
debug("block checksum %x", checksum)
|
||||
defer func() { debug("current frame checksum %x", z.checksum.Sum32()) }()
|
||||
}
|
||||
|
||||
return nil
|
||||
return z.writeUint32(checksum)
|
||||
}
|
||||
|
||||
// Flush flushes any pending compressed data to the underlying writer.
|
||||
|
@ -213,7 +298,33 @@ func (z *Writer) Flush() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
return z.compressBlock(z.data[:z.idx])
|
||||
data := z.data[:z.idx]
|
||||
z.idx = 0
|
||||
if z.c == nil {
|
||||
return z.compressBlock(data)
|
||||
}
|
||||
if !z.NoChecksum {
|
||||
_, _ = z.checksum.Write(data)
|
||||
}
|
||||
c := make(chan zResult)
|
||||
z.c <- c
|
||||
writerCompressBlock(c, z.Header, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *Writer) close() error {
|
||||
if z.c == nil {
|
||||
return nil
|
||||
}
|
||||
// Send a sentinel block (no data to compress) to terminate the writer main goroutine.
|
||||
c := make(chan zResult)
|
||||
z.c <- c
|
||||
c <- zResult{}
|
||||
// Wait for the main goroutine to complete.
|
||||
<-c
|
||||
// At this point the main goroutine has shut down or is about to return.
|
||||
z.c = nil
|
||||
return z.err
|
||||
}
|
||||
|
||||
// Close closes the Writer, flushing any unwritten data to the underlying io.Writer, but does not close the underlying io.Writer.
|
||||
|
@ -223,10 +334,13 @@ func (z *Writer) Close() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := z.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := z.close(); err != nil {
|
||||
return err
|
||||
}
|
||||
z.freeBuffers()
|
||||
|
||||
if debugFlag {
|
||||
debug("writing last empty block")
|
||||
|
@ -234,28 +348,29 @@ func (z *Writer) Close() error {
|
|||
if err := z.writeUint32(0); err != nil {
|
||||
return err
|
||||
}
|
||||
if !z.NoChecksum {
|
||||
checksum := z.checksum.Sum32()
|
||||
if debugFlag {
|
||||
debug("stream checksum %x", checksum)
|
||||
}
|
||||
if err := z.writeUint32(checksum); err != nil {
|
||||
return err
|
||||
}
|
||||
if z.NoChecksum {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
checksum := z.checksum.Sum32()
|
||||
if debugFlag {
|
||||
debug("stream checksum %x", checksum)
|
||||
}
|
||||
return z.writeUint32(checksum)
|
||||
}
|
||||
|
||||
// Reset clears the state of the Writer z such that it is equivalent to its
|
||||
// initial state from NewWriter, but instead writing to w.
|
||||
// No access to the underlying io.Writer is performed.
|
||||
func (z *Writer) Reset(w io.Writer) {
|
||||
z.Header = Header{}
|
||||
n := cap(z.c)
|
||||
_ = z.close()
|
||||
z.freeBuffers()
|
||||
z.Header.Reset()
|
||||
z.dst = w
|
||||
z.checksum.Reset()
|
||||
z.zdata = z.zdata[:0]
|
||||
z.data = z.data[:0]
|
||||
z.idx = 0
|
||||
z.err = nil
|
||||
z.WithConcurrency(n)
|
||||
}
|
||||
|
||||
// writeUint32 writes a uint32 to the underlying writer.
|
||||
|
@ -265,3 +380,29 @@ func (z *Writer) writeUint32(x uint32) error {
|
|||
_, err := z.dst.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// writerCompressBlock compresses data into a pooled buffer and writes its result
|
||||
// out to the input channel.
|
||||
func writerCompressBlock(c chan zResult, header Header, data []byte) {
|
||||
zdata := getBuffer(header.BlockMaxSize)
|
||||
// The compressed block size cannot exceed the input's.
|
||||
var zn int
|
||||
if level := header.CompressionLevel; level != 0 {
|
||||
zn, _ = CompressBlockHC(data, zdata, level)
|
||||
} else {
|
||||
var hashTable [winSize]int
|
||||
zn, _ = CompressBlock(data, zdata, hashTable[:])
|
||||
}
|
||||
var res zResult
|
||||
if zn > 0 && zn < len(data) {
|
||||
res.size = uint32(zn)
|
||||
res.data = zdata[:zn]
|
||||
} else {
|
||||
res.size = uint32(len(data)) | compressedBlockFlag
|
||||
res.data = data
|
||||
}
|
||||
if header.BlockChecksum {
|
||||
res.checksum = xxh32.ChecksumZero(res.data)
|
||||
}
|
||||
c <- res
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ var (
|
|||
oidFriendlyName = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 9, 20})
|
||||
oidLocalKeyID = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 9, 21})
|
||||
oidMicrosoftCSPName = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311, 17, 1})
|
||||
|
||||
errUnknownAttributeOID = errors.New("pkcs12: unknown attribute OID")
|
||||
)
|
||||
|
||||
type pfxPdu struct {
|
||||
|
@ -104,6 +106,11 @@ func unmarshal(in []byte, out interface{}) error {
|
|||
}
|
||||
|
||||
// ToPEM converts all "safe bags" contained in pfxData to PEM blocks.
|
||||
// Unknown attributes are discarded.
|
||||
//
|
||||
// Note that although the returned PEM blocks for private keys have type
|
||||
// "PRIVATE KEY", the bytes are not encoded according to PKCS #8, but according
|
||||
// to PKCS #1 for RSA keys and SEC 1 for ECDSA keys.
|
||||
func ToPEM(pfxData []byte, password string) ([]*pem.Block, error) {
|
||||
encodedPassword, err := bmpString(password)
|
||||
if err != nil {
|
||||
|
@ -135,6 +142,9 @@ func convertBag(bag *safeBag, password []byte) (*pem.Block, error) {
|
|||
|
||||
for _, attribute := range bag.Attributes {
|
||||
k, v, err := convertAttribute(&attribute)
|
||||
if err == errUnknownAttributeOID {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -188,7 +198,7 @@ func convertAttribute(attribute *pkcs12Attribute) (key, value string, err error)
|
|||
key = "Microsoft CSP Name"
|
||||
isString = true
|
||||
default:
|
||||
return "", "", errors.New("pkcs12: unknown attribute with OID " + attribute.Id.String())
|
||||
return "", "", errUnknownAttributeOID
|
||||
}
|
||||
|
||||
if isString {
|
||||
|
|
|
@ -36,7 +36,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
|||
|
||||
// during the authentication phase the client first attempts the "none" method
|
||||
// then any untried methods suggested by the server.
|
||||
tried := make(map[string]bool)
|
||||
var tried []string
|
||||
var lastMethods []string
|
||||
|
||||
sessionID := c.transport.getSessionID()
|
||||
|
@ -49,7 +49,9 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
|||
// success
|
||||
return nil
|
||||
} else if ok == authFailure {
|
||||
tried[auth.method()] = true
|
||||
if m := auth.method(); !contains(tried, m) {
|
||||
tried = append(tried, m)
|
||||
}
|
||||
}
|
||||
if methods == nil {
|
||||
methods = lastMethods
|
||||
|
@ -61,7 +63,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
|||
findNext:
|
||||
for _, a := range config.Auth {
|
||||
candidateMethod := a.method()
|
||||
if tried[candidateMethod] {
|
||||
if contains(tried, candidateMethod) {
|
||||
continue
|
||||
}
|
||||
for _, meth := range methods {
|
||||
|
@ -72,16 +74,16 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", keys(tried))
|
||||
return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", tried)
|
||||
}
|
||||
|
||||
func keys(m map[string]bool) []string {
|
||||
s := make([]string, 0, len(m))
|
||||
|
||||
for key := range m {
|
||||
s = append(s, key)
|
||||
func contains(list []string, e string) bool {
|
||||
for _, s := range list {
|
||||
if s == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return s
|
||||
return false
|
||||
}
|
||||
|
||||
// An AuthMethod represents an instance of an RFC 4252 authentication method.
|
||||
|
|
|
@ -107,6 +107,7 @@ func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMis
|
|||
|
||||
// dialCall is an in-flight Transport dial call to a host.
|
||||
type dialCall struct {
|
||||
_ incomparable
|
||||
p *clientConnPool
|
||||
done chan struct{} // closed when done
|
||||
res *ClientConn // valid after done is closed
|
||||
|
@ -180,6 +181,7 @@ func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn)
|
|||
}
|
||||
|
||||
type addConnCall struct {
|
||||
_ incomparable
|
||||
p *clientConnPool
|
||||
done chan struct{} // closed when done
|
||||
err error
|
||||
|
@ -200,12 +202,6 @@ func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
|
|||
close(c.done)
|
||||
}
|
||||
|
||||
func (p *clientConnPool) addConn(key string, cc *ClientConn) {
|
||||
p.mu.Lock()
|
||||
p.addConnLocked(key, cc)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// p.mu must be held
|
||||
func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) {
|
||||
for _, v := range p.conns[key] {
|
||||
|
|
|
@ -8,6 +8,8 @@ package http2
|
|||
|
||||
// flow is the flow control window's size.
|
||||
type flow struct {
|
||||
_ incomparable
|
||||
|
||||
// n is the number of DATA bytes we're allowed to send.
|
||||
// A flow is kept both on a conn and a per-stream.
|
||||
n int32
|
||||
|
|
|
@ -105,7 +105,14 @@ func huffmanDecode(buf *bytes.Buffer, maxLen int, v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// incomparable is a zero-width, non-comparable type. Adding it to a struct
|
||||
// makes that struct also non-comparable, and generally doesn't add
|
||||
// any size (as long as it's first).
|
||||
type incomparable [0]func()
|
||||
|
||||
type node struct {
|
||||
_ incomparable
|
||||
|
||||
// children is non-nil for internal nodes
|
||||
children *[256]*node
|
||||
|
||||
|
|
|
@ -241,6 +241,7 @@ func (cw closeWaiter) Wait() {
|
|||
// Its buffered writer is lazily allocated as needed, to minimize
|
||||
// idle memory usage with many connections.
|
||||
type bufferedWriter struct {
|
||||
_ incomparable
|
||||
w io.Writer // immutable
|
||||
bw *bufio.Writer // non-nil when data is buffered
|
||||
}
|
||||
|
@ -313,6 +314,7 @@ func bodyAllowedForStatus(status int) bool {
|
|||
}
|
||||
|
||||
type httpError struct {
|
||||
_ incomparable
|
||||
msg string
|
||||
timeout bool
|
||||
}
|
||||
|
@ -376,3 +378,8 @@ func (s *sorter) SortStrings(ss []string) {
|
|||
func validPseudoPath(v string) bool {
|
||||
return (len(v) > 0 && v[0] == '/') || v == "*"
|
||||
}
|
||||
|
||||
// incomparable is a zero-width, non-comparable type. Adding it to a struct
|
||||
// makes that struct also non-comparable, and generally doesn't add
|
||||
// any size (as long as it's first).
|
||||
type incomparable [0]func()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue