aboutsummaryrefslogtreecommitdiffstats
path: root/client/go
diff options
context:
space:
mode:
Diffstat (limited to 'client/go')
-rw-r--r--client/go/go.mod33
-rw-r--r--client/go/go.sum84
-rw-r--r--client/go/internal/cli/auth/zts/zts.go62
-rw-r--r--client/go/internal/cli/auth/zts/zts_test.go37
-rw-r--r--client/go/internal/cli/cmd/feed.go33
-rw-r--r--client/go/internal/cli/cmd/feed_test.go3
-rw-r--r--client/go/internal/cli/cmd/query_test.go2
-rw-r--r--client/go/internal/cli/cmd/root.go27
-rw-r--r--client/go/internal/cli/cmd/testutil_test.go2
-rw-r--r--client/go/internal/curl/curl.go4
-rw-r--r--client/go/internal/curl/curl_test.go15
-rw-r--r--client/go/internal/util/http.go8
-rw-r--r--client/go/internal/vespa/document/document.go31
-rw-r--r--client/go/internal/vespa/document/document_test.go23
-rw-r--r--client/go/internal/vespa/document/http.go3
-rw-r--r--client/go/internal/vespa/document/http_test.go4
-rw-r--r--client/go/internal/vespa/target.go3
17 files changed, 239 insertions, 135 deletions
diff --git a/client/go/go.mod b/client/go/go.mod
index 94f69c8286a..c70ee5b75c8 100644
--- a/client/go/go.mod
+++ b/client/go/go.mod
@@ -3,32 +3,33 @@ module github.com/vespa-engine/vespa/client/go
go 1.18
require (
- github.com/briandowns/spinner v1.16.0
- github.com/fatih/color v1.10.0
- github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
- github.com/mattn/go-colorable v0.1.8
- github.com/mattn/go-isatty v0.0.13
- github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2
- github.com/spf13/cobra v1.4.0
+ github.com/alessio/shellescape v1.4.1
+ github.com/briandowns/spinner v1.23.0
+ github.com/fatih/color v1.15.0
+ github.com/goccy/go-json v0.10.2
+ github.com/klauspost/compress v1.16.5
+ github.com/mattn/go-colorable v0.1.13
+ github.com/mattn/go-isatty v0.0.18
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
+ github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
- github.com/stretchr/testify v1.7.0
- github.com/zalando/go-keyring v0.1.1
+ github.com/stretchr/testify v1.8.2
+ github.com/zalando/go-keyring v0.2.2
golang.org/x/net v0.9.0
golang.org/x/sys v0.7.0
- gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+ gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
- github.com/danieljoos/wincred v1.1.0 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/godbus/dbus/v5 v5.0.4 // indirect
- github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/stretchr/objx v0.1.1 // indirect
+ golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/client/go/go.sum b/client/go/go.sum
index ac662c9fd43..9b79c215864 100644
--- a/client/go/go.sum
+++ b/client/go/go.sum
@@ -1,68 +1,70 @@
-github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs=
-github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
-github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
-github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
-github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
+github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
+github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
+github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
+github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
+github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
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/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
-github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
-github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
-github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE=
-github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5RI/4=
+github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/client/go/internal/cli/auth/zts/zts.go b/client/go/internal/cli/auth/zts/zts.go
index 2c66ff13e8b..0f73ea5912d 100644
--- a/client/go/internal/cli/auth/zts/zts.go
+++ b/client/go/internal/cli/auth/zts/zts.go
@@ -3,23 +3,39 @@ package zts
import (
"encoding/json"
"fmt"
+ "io"
"net/http"
"net/url"
"strings"
+ "sync"
"time"
"github.com/vespa-engine/vespa/client/go/internal/util"
)
-const DefaultURL = "https://zts.athenz.ouroath.com:4443"
+const (
+ DefaultURL = "https://zts.athenz.ouroath.com:4443"
+ expirySlack = 5 * time.Minute
+)
// Client is a client for Athenz ZTS, an authentication token service.
type Client struct {
client util.HTTPClient
tokenURL *url.URL
domain string
+ now func() time.Time
+ token Token
+ mu sync.Mutex
+}
+
+// Token is an access token retrieved from ZTS.
+type Token struct {
+ Value string
+ ExpiresAt time.Time
}
+func (t *Token) isExpired(now time.Time) bool { return t.ExpiresAt.Sub(now) < expirySlack }
+
// NewClient creates a new client for an Athenz ZTS service located at serviceURL.
func NewClient(client util.HTTPClient, domain, serviceURL string) (*Client, error) {
tokenURL, err := url.Parse(serviceURL)
@@ -27,44 +43,60 @@ func NewClient(client util.HTTPClient, domain, serviceURL string) (*Client, erro
return nil, err
}
tokenURL.Path = "/zts/v1/oauth2/token"
- return &Client{tokenURL: tokenURL, client: client, domain: domain}, nil
+ return &Client{tokenURL: tokenURL, client: client, domain: domain, now: time.Now}, nil
}
func (c *Client) Authenticate(request *http.Request) error {
- accessToken, err := c.AccessToken()
- if err != nil {
- return err
+ now := c.now()
+ if c.token.isExpired(now) {
+ c.mu.Lock()
+ if c.token.isExpired(now) {
+ accessToken, err := c.AccessToken()
+ if err != nil {
+ c.mu.Unlock()
+ return err
+ }
+ c.token = accessToken
+ }
+ c.mu.Unlock()
}
if request.Header == nil {
request.Header = make(http.Header)
}
- request.Header.Add("Authorization", "Bearer "+accessToken)
+ request.Header.Add("Authorization", "Bearer "+c.token.Value)
return nil
}
// AccessToken returns an access token within the domain configured in client c.
-func (c *Client) AccessToken() (string, error) {
- // TODO(mpolden): This should cache and re-use tokens until expiry
+func (c *Client) AccessToken() (Token, error) {
data := fmt.Sprintf("grant_type=client_credentials&scope=%s:domain", c.domain)
req, err := http.NewRequest("POST", c.tokenURL.String(), strings.NewReader(data))
if err != nil {
- return "", err
+ return Token{}, err
}
+ now := c.now()
response, err := c.client.Do(req, 10*time.Second)
if err != nil {
- return "", err
+ return Token{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
- return "", fmt.Errorf("zts: got status %d from %s", response.StatusCode, c.tokenURL.String())
+ return Token{}, fmt.Errorf("zts: got status %d from %s", response.StatusCode, c.tokenURL.String())
}
var ztsResponse struct {
AccessToken string `json:"access_token"`
+ ExpirySecs int `json:"expires_in"`
+ }
+ b, err := io.ReadAll(response.Body)
+ if err != nil {
+ return Token{}, err
}
- dec := json.NewDecoder(response.Body)
- if err := dec.Decode(&ztsResponse); err != nil {
- return "", err
+ if err := json.Unmarshal(b, &ztsResponse); err != nil {
+ return Token{}, err
}
- return ztsResponse.AccessToken, nil
+ return Token{
+ Value: ztsResponse.AccessToken,
+ ExpiresAt: now.Add(time.Duration(ztsResponse.ExpirySecs) * time.Second),
+ }, nil
}
diff --git a/client/go/internal/cli/auth/zts/zts_test.go b/client/go/internal/cli/auth/zts/zts_test.go
index 1c75a94ee03..15c60ed46d7 100644
--- a/client/go/internal/cli/auth/zts/zts_test.go
+++ b/client/go/internal/cli/auth/zts/zts_test.go
@@ -2,28 +2,57 @@ package zts
import (
"testing"
+ "time"
"github.com/vespa-engine/vespa/client/go/internal/mock"
)
+type manualClock struct{ t time.Time }
+
+func (c *manualClock) now() time.Time { return c.t }
+func (c *manualClock) advance(d time.Duration) { c.t = c.t.Add(d) }
+
func TestAccessToken(t *testing.T) {
httpClient := mock.HTTPClient{}
client, err := NewClient(&httpClient, "vespa.vespa", "http://example.com")
if err != nil {
t.Fatal(err)
}
+ clock := &manualClock{t: time.Now()}
+ client.now = clock.now
httpClient.NextResponseString(400, `{"message": "bad request"}`)
_, err = client.AccessToken()
if err == nil {
t.Fatal("want error for non-ok response status")
}
- httpClient.NextResponseString(200, `{"access_token": "foo bar"}`)
+ httpClient.NextResponseString(200, `{"access_token": "foo", "expires_in": 3600}`)
token, err := client.AccessToken()
if err != nil {
t.Fatal(err)
}
- want := "foo bar"
- if token != want {
- t.Errorf("got %q, want %q", token, want)
+
+ // Token is cached
+ expiresAt := clock.now().Add(time.Hour)
+ assertToken(t, Token{Value: "foo", ExpiresAt: expiresAt}, token)
+ clock.advance(54 * time.Minute)
+ assertToken(t, Token{Value: "foo", ExpiresAt: expiresAt}, token)
+
+ // Token is renewed when nearing expiry
+ clock.advance(time.Minute + time.Second)
+ httpClient.NextResponseString(200, `{"access_token": "bar", "expires_in": 1800}`)
+ token, err = client.AccessToken()
+ if err != nil {
+ t.Fatal(err)
+ }
+ expiresAt = clock.now().Add(30 * time.Minute)
+ assertToken(t, Token{Value: "bar", ExpiresAt: expiresAt}, token)
+}
+
+func assertToken(t *testing.T, want, got Token) {
+ if want.Value != got.Value {
+ t.Errorf("got Value=%q, want %q", got.Value, want.Value)
+ }
+ if want.ExpiresAt != got.ExpiresAt {
+ t.Errorf("got ExpiresAt=%s, want %s", got.ExpiresAt, want.ExpiresAt)
}
}
diff --git a/client/go/internal/cli/cmd/feed.go b/client/go/internal/cli/cmd/feed.go
index 5b168ef79a2..2ea3ee1c4ed 100644
--- a/client/go/internal/cli/cmd/feed.go
+++ b/client/go/internal/cli/cmd/feed.go
@@ -93,15 +93,30 @@ $ cat docs.jsonl | vespa feed -`,
return cmd
}
-func createServiceClients(service *vespa.Service, n int) []util.HTTPClient {
- clients := make([]util.HTTPClient, 0, n)
+func createServices(n int, timeout time.Duration, cli *CLI) ([]util.HTTPClient, string, error) {
+ if n < 1 {
+ return nil, "", fmt.Errorf("need at least one client")
+ }
+ target, err := cli.target(targetOptions{})
+ if err != nil {
+ return nil, "", err
+ }
+ services := make([]util.HTTPClient, 0, n)
+ baseURL := ""
for i := 0; i < n; i++ {
- client := service.Client().Clone()
+ service, err := cli.service(target, vespa.DocumentService, 0, cli.config.cluster())
+ if err != nil {
+ return nil, "", err
+ }
+ baseURL = service.BaseURL
+ // Create a separate HTTP client for each service
+ client := cli.httpClientFactory(timeout)
// Feeding should always use HTTP/2
util.ForceHTTP2(client, service.TLSOptions.KeyPair, service.TLSOptions.CACertificate, service.TLSOptions.TrustAll)
- clients = append(clients, client)
+ service.SetClient(client)
+ services = append(services, service)
}
- return clients
+ return services, baseURL, nil
}
func summaryTicker(secs int, cli *CLI, start time.Time, statsFunc func() document.Stats) *time.Ticker {
@@ -130,21 +145,21 @@ func (opts feedOptions) compressionMode() (document.Compression, error) {
}
func feed(files []string, options feedOptions, cli *CLI) error {
- service, err := documentService(cli)
+ timeout := time.Duration(options.timeoutSecs) * time.Second
+ clients, baseURL, err := createServices(options.connections, timeout, cli)
if err != nil {
return err
}
- clients := createServiceClients(service, options.connections)
compression, err := options.compressionMode()
if err != nil {
return err
}
client, err := document.NewClient(document.ClientOptions{
Compression: compression,
- Timeout: time.Duration(options.timeoutSecs) * time.Second,
+ Timeout: timeout,
Route: options.route,
TraceLevel: options.traceLevel,
- BaseURL: service.BaseURL,
+ BaseURL: baseURL,
NowFunc: cli.now,
}, clients)
if err != nil {
diff --git a/client/go/internal/cli/cmd/feed_test.go b/client/go/internal/cli/cmd/feed_test.go
index 467d55a0a6e..097d4ae5fa3 100644
--- a/client/go/internal/cli/cmd/feed_test.go
+++ b/client/go/internal/cli/cmd/feed_test.go
@@ -24,10 +24,9 @@ func (c *manualClock) now() time.Time {
}
func TestFeed(t *testing.T) {
- httpClient := &mock.HTTPClient{}
clock := &manualClock{tick: time.Second}
cli, stdout, stderr := newTestCLI(t)
- cli.httpClient = httpClient
+ httpClient := cli.httpClient.(*mock.HTTPClient)
cli.now = clock.now
td := t.TempDir()
diff --git a/client/go/internal/cli/cmd/query_test.go b/client/go/internal/cli/cmd/query_test.go
index 94b5a485b9d..1caf6d33e70 100644
--- a/client/go/internal/cli/cmd/query_test.go
+++ b/client/go/internal/cli/cmd/query_test.go
@@ -27,7 +27,7 @@ func TestQueryVerbose(t *testing.T) {
cli.httpClient = client
assert.Nil(t, cli.Run("query", "-v", "select from sources * where title contains 'foo'"))
- assert.Equal(t, "curl http://127.0.0.1:8080/search/\\?timeout=10s\\&yql=select+from+sources+%2A+where+title+contains+%27foo%27\n", stderr.String())
+ assert.Equal(t, "curl 'http://127.0.0.1:8080/search/?timeout=10s&yql=select+from+sources+%2A+where+title+contains+%27foo%27'\n", stderr.String())
assert.Equal(t, "{\n \"query\": \"result\"\n}\n", stdout.String())
}
diff --git a/client/go/internal/cli/cmd/root.go b/client/go/internal/cli/cmd/root.go
index 1b37ff00269..c4012024426 100644
--- a/client/go/internal/cli/cmd/root.go
+++ b/client/go/internal/cli/cmd/root.go
@@ -47,13 +47,14 @@ type CLI struct {
config *Config
version version.Version
- httpClient util.HTTPClient
- auth0Factory auth0Factory
- ztsFactory ztsFactory
- exec executor
- isTerminal func() bool
- spinner func(w io.Writer, message string, fn func() error) error
- now func() time.Time
+ httpClient util.HTTPClient
+ httpClientFactory func(timeout time.Duration) util.HTTPClient
+ auth0Factory auth0Factory
+ ztsFactory ztsFactory
+ exec executor
+ isTerminal func() bool
+ spinner func(w io.Writer, message string, fn func() error) error
+ now func() time.Time
}
// ErrCLI is an error returned to the user. It wraps an exit status, a regular error and optional hints for resolving
@@ -122,17 +123,19 @@ For detailed description of flags and configuration, see 'vespa help config'.
if err != nil {
return nil, err
}
+ httpClientFactory := util.CreateClient
cli := CLI{
Environment: env,
Stdin: os.Stdin,
Stdout: stdout,
Stderr: stderr,
- version: version,
- cmd: cmd,
- httpClient: util.CreateClient(time.Second * 10),
- exec: &execSubprocess{},
- now: time.Now,
+ version: version,
+ cmd: cmd,
+ httpClient: httpClientFactory(time.Second * 10),
+ httpClientFactory: httpClientFactory,
+ exec: &execSubprocess{},
+ now: time.Now,
auth0Factory: func(httpClient util.HTTPClient, options auth0.Options) (vespa.Authenticator, error) {
return auth0.NewClient(httpClient, options)
},
diff --git a/client/go/internal/cli/cmd/testutil_test.go b/client/go/internal/cli/cmd/testutil_test.go
index 492e40d8855..61d6c15c5a0 100644
--- a/client/go/internal/cli/cmd/testutil_test.go
+++ b/client/go/internal/cli/cmd/testutil_test.go
@@ -6,6 +6,7 @@ import (
"net/http"
"path/filepath"
"testing"
+ "time"
"github.com/vespa-engine/vespa/client/go/internal/cli/auth/auth0"
"github.com/vespa-engine/vespa/client/go/internal/mock"
@@ -28,6 +29,7 @@ func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Bu
t.Fatal(err)
}
httpClient := &mock.HTTPClient{}
+ cli.httpClientFactory = func(timeout time.Duration) util.HTTPClient { return httpClient }
cli.httpClient = httpClient
cli.exec = &mock.Exec{}
cli.auth0Factory = func(httpClient util.HTTPClient, options auth0.Options) (vespa.Authenticator, error) {
diff --git a/client/go/internal/curl/curl.go b/client/go/internal/curl/curl.go
index b70e0f824a3..daa60e6ff14 100644
--- a/client/go/internal/curl/curl.go
+++ b/client/go/internal/curl/curl.go
@@ -7,7 +7,7 @@ import (
"os/exec"
"runtime"
- "github.com/kballard/go-shellquote"
+ "github.com/alessio/shellescape"
"github.com/vespa-engine/vespa/client/go/internal/util"
)
@@ -77,7 +77,7 @@ func (c *Command) Args() []string {
func (c *Command) String() string {
args := []string{c.Path}
args = append(args, c.Args()...)
- return shellquote.Join(args...)
+ return shellescape.QuoteCommand(args)
}
func (c *Command) Header(key, value string) {
diff --git a/client/go/internal/curl/curl_test.go b/client/go/internal/curl/curl_test.go
index 448e1e5199f..c8bcfb7f56b 100644
--- a/client/go/internal/curl/curl_test.go
+++ b/client/go/internal/curl/curl_test.go
@@ -5,13 +5,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestPost(t *testing.T) {
c, err := Post("https://example.com")
- if err != nil {
- t.Fatal(err)
- }
+ require.Nil(t, err)
c.PrivateKey = "key.pem"
c.Certificate = "cert.pem"
c.WithBodyFile("file.json")
@@ -22,22 +21,18 @@ func TestPost(t *testing.T) {
func TestGet(t *testing.T) {
c, err := Get("https://example.com")
- if err != nil {
- t.Fatal(err)
- }
+ require.Nil(t, err)
c.PrivateKey = "key.pem"
c.Certificate = "cert.pem"
c.Param("yql", "select * from sources * where title contains 'foo';")
c.Param("hits", "5")
- assert.Equal(t, `curl --key key.pem --cert cert.pem https://example.com\?hits=5\&yql=select+%2A+from+sources+%2A+where+title+contains+%27foo%27%3B`, c.String())
+ assert.Equal(t, `curl --key key.pem --cert cert.pem 'https://example.com?hits=5&yql=select+%2A+from+sources+%2A+where+title+contains+%27foo%27%3B'`, c.String())
}
func TestRawArgs(t *testing.T) {
c, err := RawArgs("https://example.com/search", "-v", "-m", "10", "-H", "foo: bar")
- if err != nil {
- t.Fatal(err)
- }
+ assert.Nil(t, err)
c.PrivateKey = "key.pem"
c.Certificate = "cert.pem"
diff --git a/client/go/internal/util/http.go b/client/go/internal/util/http.go
index 8a67b24dffb..546098a204d 100644
--- a/client/go/internal/util/http.go
+++ b/client/go/internal/util/http.go
@@ -16,7 +16,6 @@ import (
type HTTPClient interface {
Do(request *http.Request, timeout time.Duration) (response *http.Response, error error)
- Clone() HTTPClient
}
type defaultHTTPClient struct {
@@ -34,8 +33,6 @@ func (c *defaultHTTPClient) Do(request *http.Request, timeout time.Duration) (re
return c.client.Do(request)
}
-func (c *defaultHTTPClient) Clone() HTTPClient { return CreateClient(c.client.Timeout) }
-
func ConfigureTLS(client HTTPClient, certificates []tls.Certificate, caCertificate []byte, trustAll bool) {
c, ok := client.(*defaultHTTPClient)
if !ok {
@@ -81,8 +78,9 @@ func ForceHTTP2(client HTTPClient, certificates []tls.Certificate, caCertificate
// https://github.com/golang/go/issues/16582
// https://github.com/golang/go/issues/22091
c.client.Transport = &http2.Transport{
- AllowHTTP: true,
- DialTLSContext: dialFunc,
+ DisableCompression: true,
+ AllowHTTP: true,
+ DialTLSContext: dialFunc,
}
ConfigureTLS(client, certificates, caCertificate, trustAll)
}
diff --git a/client/go/internal/vespa/document/document.go b/client/go/internal/vespa/document/document.go
index 214d1dc4797..8bc897d49b3 100644
--- a/client/go/internal/vespa/document/document.go
+++ b/client/go/internal/vespa/document/document.go
@@ -2,11 +2,12 @@ package document
import (
"bufio"
- "encoding/json"
"fmt"
"io"
"strconv"
"strings"
+
+ "github.com/goccy/go-json"
)
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
@@ -158,22 +159,25 @@ func (d *Decoder) guessMode() error {
if err := d.buf.UnreadByte(); err != nil {
return err
}
- if d.array {
- // prepare for decoding objects inside array
- if _, err := d.dec.Token(); err != nil {
- return err
- }
+ if err := d.readArrayToken(true); err != nil {
+ return err
}
}
return nil
}
-func (d *Decoder) readCloseToken() error {
+func (d *Decoder) readArrayToken(open bool) error {
if !d.array {
return nil
}
- _, err := d.dec.Token()
- return err
+ t, err := d.dec.Token()
+ if err != nil {
+ return err
+ }
+ if (open && t == json.Delim('[')) || (!open && t == json.Delim(']')) {
+ return nil
+ }
+ return fmt.Errorf("invalid array token: %q", t)
}
func (d *Decoder) Decode() (Document, error) {
@@ -189,11 +193,10 @@ func (d *Decoder) decode() (Document, error) {
return Document{}, err
}
if !d.dec.More() {
- err := io.EOF
- if tokenErr := d.readCloseToken(); tokenErr != nil {
- err = tokenErr
+ if err := d.readArrayToken(false); err != nil {
+ return Document{}, err
}
- return Document{}, err
+ return Document{}, io.EOF
}
doc := jsonDocument{}
if err := d.dec.Decode(&doc); err != nil {
@@ -203,7 +206,7 @@ func (d *Decoder) decode() (Document, error) {
}
func NewDecoder(r io.Reader) *Decoder {
- buf := bufio.NewReader(r)
+ buf := bufio.NewReaderSize(r, 1<<26)
return &Decoder{
buf: buf,
dec: json.NewDecoder(buf),
diff --git a/client/go/internal/vespa/document/document_test.go b/client/go/internal/vespa/document/document_test.go
index 111b9e37acc..bdf18586753 100644
--- a/client/go/internal/vespa/document/document_test.go
+++ b/client/go/internal/vespa/document/document_test.go
@@ -1,6 +1,7 @@
package document
import (
+ "fmt"
"io"
"reflect"
"strings"
@@ -173,8 +174,28 @@ func TestDocumentDecoder(t *testing.T) {
t.Errorf("unexpected error: %s", err)
}
_, err = r.Decode()
- wantErr := "invalid json at byte offset 60: invalid character '\\n' in string literal"
+ wantErr := "invalid json at byte offset 122: json: string of object unexpected end of JSON input"
if err.Error() != wantErr {
t.Errorf("want error %q, got %q", wantErr, err.Error())
}
}
+
+func benchmarkDocumentDecoder(b *testing.B, size int) {
+ b.Helper()
+ input := fmt.Sprintf(`{"put": "id:ns:type::doc1", "fields": {"foo": "%s"}}`, strings.Repeat("s", size))
+ r := strings.NewReader(input)
+ dec := NewDecoder(r)
+ b.ResetTimer() // ignore setup time
+
+ for n := 0; n < b.N; n++ {
+ _, err := dec.Decode()
+ if err != nil {
+ b.Fatal(err)
+ }
+ r.Seek(0, 0)
+ }
+}
+
+func BenchmarkDocumentDecoderSmall(b *testing.B) { benchmarkDocumentDecoder(b, 10) }
+
+func BenchmarkDocumentDecoderLarge(b *testing.B) { benchmarkDocumentDecoder(b, 10000) }
diff --git a/client/go/internal/vespa/document/http.go b/client/go/internal/vespa/document/http.go
index d42615d1e71..3581a791dbe 100644
--- a/client/go/internal/vespa/document/http.go
+++ b/client/go/internal/vespa/document/http.go
@@ -2,7 +2,6 @@ package document
import (
"bytes"
- "compress/gzip"
"encoding/json"
"fmt"
"io"
@@ -15,6 +14,8 @@ import (
"sync/atomic"
"time"
+ "github.com/klauspost/compress/gzip"
+
"github.com/vespa-engine/vespa/client/go/internal/util"
)
diff --git a/client/go/internal/vespa/document/http_test.go b/client/go/internal/vespa/document/http_test.go
index 7e858f96020..489460b3ed7 100644
--- a/client/go/internal/vespa/document/http_test.go
+++ b/client/go/internal/vespa/document/http_test.go
@@ -275,6 +275,7 @@ func TestClientMethodAndURL(t *testing.T) {
}
func benchmarkClientSend(b *testing.B, compression Compression, document Document) {
+ b.Helper()
httpClient := mock.HTTPClient{}
client, _ := NewClient(ClientOptions{
Compression: compression,
@@ -293,6 +294,7 @@ func BenchmarkClientSend(b *testing.B) {
}
func BenchmarkClientSendCompressed(b *testing.B) {
- doc := Document{Create: true, Id: mustParseId("id:ns:type::doc1"), Operation: OperationUpdate, Body: []byte(`{"fields":{"foo": "my document"}}`)}
+ body := fmt.Sprintf(`{"fields":{"foo": "%s"}}`, strings.Repeat("my document", 100))
+ doc := Document{Create: true, Id: mustParseId("id:ns:type::doc1"), Operation: OperationUpdate, Body: []byte(body)}
benchmarkClientSend(b, CompressionGzip, doc)
}
diff --git a/client/go/internal/vespa/target.go b/client/go/internal/vespa/target.go
index 9f3fd7f5c65..6dd64dd1275 100644
--- a/client/go/internal/vespa/target.go
+++ b/client/go/internal/vespa/target.go
@@ -110,7 +110,8 @@ func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Respon
return s.httpClient.Do(request, timeout)
}
-func (s *Service) Client() util.HTTPClient { return s.httpClient }
+// SetClient sets the HTTP client that this service should use.
+func (s *Service) SetClient(client util.HTTPClient) { s.httpClient = client }
// Wait polls the health check of this service until it succeeds or timeout passes.
func (s *Service) Wait(timeout time.Duration) (int, error) {