mirror of https://github.com/status-im/consul.git
Update vault CA for latest api client
This commit is contained in:
parent
74dc50a771
commit
1cd7c43544
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
connect: The Vault provider will now automatically renew the lease of the token used, if supported.
|
||||
```
|
|
@ -92,7 +92,7 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
|||
|
||||
// Set up a renewer to renew the token automatically, if supported.
|
||||
if token.Renewable {
|
||||
renewer, err := client.NewRenewer(&vaultapi.RenewerInput{
|
||||
lifetimeWatcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
|
||||
Secret: &vaultapi.Secret{
|
||||
Auth: &vaultapi.SecretAuth{
|
||||
ClientToken: config.Token,
|
||||
|
@ -100,7 +100,8 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
|||
LeaseDuration: secret.LeaseDuration,
|
||||
},
|
||||
},
|
||||
Increment: token.TTL,
|
||||
Increment: token.TTL,
|
||||
RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error beginning Vault provider token renewal: %v", err)
|
||||
|
@ -108,31 +109,31 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
|||
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
v.shutdown = cancel
|
||||
go v.renewToken(ctx, renewer)
|
||||
go v.renewToken(ctx, lifetimeWatcher)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renewToken uses a vaultapi.Renewer to repeatedly renew our token's lease.
|
||||
func (v *VaultProvider) renewToken(ctx context.Context, renewer *vaultapi.Renewer) {
|
||||
go renewer.Renew()
|
||||
defer renewer.Stop()
|
||||
func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.LifetimeWatcher) {
|
||||
go watcher.Start()
|
||||
defer watcher.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case err := <-renewer.DoneCh():
|
||||
case err := <-watcher.DoneCh():
|
||||
if err != nil {
|
||||
v.logger.Error("Error renewing token for Vault provider", "error", err)
|
||||
}
|
||||
|
||||
// Renewer routine has finished, so start it again.
|
||||
go renewer.Renew()
|
||||
// Watcher routine has finished, so start it again.
|
||||
go watcher.Start()
|
||||
|
||||
case <-renewer.RenewCh():
|
||||
case <-watcher.RenewCh():
|
||||
v.logger.Error("Successfully renewed token for Vault provider")
|
||||
}
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -22,7 +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/frankban/quicktest v1.11.0 // 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
|
||||
|
@ -93,7 +93,6 @@ require (
|
|||
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/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
|
||||
|
|
24
go.sum
24
go.sum
|
@ -137,6 +137,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||
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/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.11.0 h1:Yyrghcw93e1jKo4DTZkRFTTFvBsVhzbblBUPNU1vW6Q=
|
||||
github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
|
@ -145,7 +147,6 @@ github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro
|
|||
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=
|
||||
|
@ -188,6 +189,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
|
@ -229,7 +232,6 @@ github.com/hashicorp/go-connlimit v0.3.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VV
|
|||
github.com/hashicorp/go-discover v0.0.0-20200501174627-ad1e96bde088 h1:jBvElOilnIl6mm8S6gva/dfeTJCcMs9TGO6/2C6k52E=
|
||||
github.com/hashicorp/go-discover v0.0.0-20200501174627-ad1e96bde088/go.mod h1:vZu6Opqf49xX5lsFAu7iFNewkcVF1sn/wyapZh5ytlg=
|
||||
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=
|
||||
|
@ -253,13 +255,10 @@ github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn
|
|||
github.com/hashicorp/go-raftchunking v0.6.1 h1:moEnaG3gcwsWNyIBJoD5PCByE+Ewkqxh6N05CT+MbwA=
|
||||
github.com/hashicorp/go-raftchunking v0.6.1/go.mod h1:cGlg3JtDy7qy6c/3Bu660Mic1JF+7lWqIwCFSb08fX0=
|
||||
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=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
|
@ -300,12 +299,8 @@ github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM=
|
|||
github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
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=
|
||||
|
@ -346,6 +341,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/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=
|
||||
|
@ -555,8 +552,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
|
||||
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=
|
||||
|
@ -646,7 +641,6 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.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=
|
||||
|
@ -688,7 +682,6 @@ google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOO
|
|||
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-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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
@ -705,7 +698,6 @@ google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
|||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
@ -716,10 +708,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
|||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
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=
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
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
|
@ -1,337 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,353 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
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"`
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
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}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,229 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
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
|
||||
)
|
|
@ -1,131 +0,0 @@
|
|||
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=
|
|
@ -1,375 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,290 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,406 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,530 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,243 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
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())
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,254 +0,0 @@
|
|||
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,
|
||||
})
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
//+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)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
//+build !linux,!darwin
|
||||
|
||||
package freeport
|
||||
|
||||
func getEphemeralPortRange() (int, int, error) {
|
||||
return 0, 0, nil
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
//+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)
|
||||
}
|
|
@ -1,391 +0,0 @@
|
|||
// 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 }
|
|
@ -1,11 +0,0 @@
|
|||
// +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
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
// +build windows
|
||||
|
||||
package freeport
|
||||
|
||||
func systemLimit() (int, error) {
|
||||
return 0, nil
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
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"))
|
||||
}
|
||||
```
|
|
@ -1,19 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,514 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -191,13 +191,6 @@ github.com/gophercloud/gophercloud/openstack/identity/v2/tokens
|
|||
github.com/gophercloud/gophercloud/openstack/identity/v3/tokens
|
||||
github.com/gophercloud/gophercloud/openstack/utils
|
||||
github.com/gophercloud/gophercloud/pagination
|
||||
# github.com/hashicorp/consul/api v1.6.0 => ./api
|
||||
github.com/hashicorp/consul/api
|
||||
github.com/hashicorp/consul/api/watch
|
||||
# github.com/hashicorp/consul/sdk v0.6.0 => ./sdk
|
||||
github.com/hashicorp/consul/sdk/freeport
|
||||
github.com/hashicorp/consul/sdk/testutil
|
||||
github.com/hashicorp/consul/sdk/testutil/retry
|
||||
# github.com/hashicorp/errwrap v1.0.0
|
||||
github.com/hashicorp/errwrap
|
||||
# github.com/hashicorp/go-bexpr v0.1.2
|
||||
|
|
Loading…
Reference in New Issue