Provisioning: Use inline secrets for gitsync (#109908)
Co-authored-by: Clarity-89 <homes89@ukr.net> Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
This commit is contained in:
co-authored by
Clarity-89
Roberto Jimenez Sanchez
parent
04f392d37b
commit
ce65391067
@@ -41,6 +41,7 @@ require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
|
||||
@@ -141,6 +141,7 @@ github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgX
|
||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0=
|
||||
github.com/Yiling-J/theine-go v0.6.1 h1:njE/rBBviU/Sq2G7PJKdLdwXg8j1azvZQulIjmshD+o=
|
||||
github.com/Yiling-J/theine-go v0.6.1/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -431,6 +432,7 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
@@ -543,6 +545,7 @@ github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJD
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs=
|
||||
github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -840,6 +843,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@@ -979,6 +983,7 @@ github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
|
||||
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw=
|
||||
github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
@@ -1369,7 +1374,10 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
@@ -1381,6 +1389,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1641,9 +1650,11 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
@@ -1715,8 +1726,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -1912,6 +1927,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@@ -2000,6 +2016,7 @@ modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
|
||||
@@ -21,12 +21,11 @@ type Repository struct {
|
||||
Status RepositoryStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// NOT YET USED FOR REAL -- testing secure value workflow
|
||||
type SecureValues struct {
|
||||
// Token used to connect the configured repository
|
||||
Token common.InlineSecureValue `json:"token,omitzero,omitempty"`
|
||||
|
||||
// Some webhooks (github) require a secret key value
|
||||
// Some webhooks (including github) require a secret key value
|
||||
WebhookSecret common.InlineSecureValue `json:"webhookSecret,omitzero,omitempty"`
|
||||
}
|
||||
|
||||
@@ -55,11 +54,6 @@ type GitHubRepositoryConfig struct {
|
||||
|
||||
// The branch to use in the repository.
|
||||
Branch string `json:"branch"`
|
||||
// Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.
|
||||
Token string `json:"token,omitempty"`
|
||||
// Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.
|
||||
// +listType=atomic
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
|
||||
// Whether we should show dashboard previews for pull requests.
|
||||
// By default, this is false (i.e. we will not create previews).
|
||||
@@ -80,11 +74,6 @@ type GitRepositoryConfig struct {
|
||||
Branch string `json:"branch"`
|
||||
// TokenUser is the user that will be used to access the repository if it's a personal access token.
|
||||
TokenUser string `json:"tokenUser,omitempty"`
|
||||
// Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.
|
||||
Token string `json:"token,omitempty"`
|
||||
// Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.
|
||||
// +listType=atomic
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
// Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.
|
||||
// This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed.
|
||||
// The path is relative to the root of the repository, regardless of the leading slash.
|
||||
@@ -100,11 +89,6 @@ type BitbucketRepositoryConfig struct {
|
||||
Branch string `json:"branch"`
|
||||
// TokenUser is the user that will be used to access the repository if it's a personal access token.
|
||||
TokenUser string `json:"tokenUser,omitempty"`
|
||||
// Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.
|
||||
Token string `json:"token,omitempty"`
|
||||
// Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.
|
||||
// +listType=atomic
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
// Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.
|
||||
// This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed.
|
||||
// The path is relative to the root of the repository, regardless of the leading slash.
|
||||
@@ -118,11 +102,6 @@ type GitLabRepositoryConfig struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
// The branch to use in the repository.
|
||||
Branch string `json:"branch"`
|
||||
// Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.
|
||||
Token string `json:"token,omitempty"`
|
||||
// Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.
|
||||
// +listType=atomic
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
// Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.
|
||||
// This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed.
|
||||
// The path is relative to the root of the repository, regardless of the leading slash.
|
||||
@@ -297,8 +276,6 @@ type SyncStatus struct {
|
||||
type WebhookStatus struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
EncryptedSecret []byte `json:"encryptedSecret,omitempty"`
|
||||
SubscribedEvents []string `json:"subscribedEvents,omitempty"`
|
||||
LastEvent int64 `json:"lastEvent,omitempty"`
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ func (in *Author) DeepCopy() *Author {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BitbucketRepositoryConfig) DeepCopyInto(out *BitbucketRepositoryConfig) {
|
||||
*out = *in
|
||||
if in.EncryptedToken != nil {
|
||||
in, out := &in.EncryptedToken, &out.EncryptedToken
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,11 +151,6 @@ func (in *FileList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitHubRepositoryConfig) DeepCopyInto(out *GitHubRepositoryConfig) {
|
||||
*out = *in
|
||||
if in.EncryptedToken != nil {
|
||||
in, out := &in.EncryptedToken, &out.EncryptedToken
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,11 +167,6 @@ func (in *GitHubRepositoryConfig) DeepCopy() *GitHubRepositoryConfig {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitLabRepositoryConfig) DeepCopyInto(out *GitLabRepositoryConfig) {
|
||||
*out = *in
|
||||
if in.EncryptedToken != nil {
|
||||
in, out := &in.EncryptedToken, &out.EncryptedToken
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,11 +183,6 @@ func (in *GitLabRepositoryConfig) DeepCopy() *GitLabRepositoryConfig {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitRepositoryConfig) DeepCopyInto(out *GitRepositoryConfig) {
|
||||
*out = *in
|
||||
if in.EncryptedToken != nil {
|
||||
in, out := &in.EncryptedToken, &out.EncryptedToken
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -738,22 +718,22 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) {
|
||||
if in.GitHub != nil {
|
||||
in, out := &in.GitHub, &out.GitHub
|
||||
*out = new(GitHubRepositoryConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
**out = **in
|
||||
}
|
||||
if in.Git != nil {
|
||||
in, out := &in.Git, &out.Git
|
||||
*out = new(GitRepositoryConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
**out = **in
|
||||
}
|
||||
if in.Bitbucket != nil {
|
||||
in, out := &in.Bitbucket, &out.Bitbucket
|
||||
*out = new(BitbucketRepositoryConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
**out = **in
|
||||
}
|
||||
if in.GitLab != nil {
|
||||
in, out := &in.GitLab, &out.GitLab
|
||||
*out = new(GitLabRepositoryConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1220,11 +1200,6 @@ func (in *WebhookResponse) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *WebhookStatus) DeepCopyInto(out *WebhookStatus) {
|
||||
*out = *in
|
||||
if in.EncryptedSecret != nil {
|
||||
in, out := &in.EncryptedSecret, &out.EncryptedSecret
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.SubscribedEvents != nil {
|
||||
in, out := &in.SubscribedEvents, &out.SubscribedEvents
|
||||
*out = make([]string, len(*in))
|
||||
|
||||
@@ -128,25 +128,6 @@ func schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref common.
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"encryptedToken": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
Type: []string{"string"},
|
||||
Format: "byte",
|
||||
},
|
||||
},
|
||||
"path": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
@@ -402,25 +383,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"encryptedToken": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
Type: []string{"string"},
|
||||
Format: "byte",
|
||||
},
|
||||
},
|
||||
"generateDashboardPreviews": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).",
|
||||
@@ -463,25 +425,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitLabRepositoryConfig(ref common.Ref
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"encryptedToken": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
Type: []string{"string"},
|
||||
Format: "byte",
|
||||
},
|
||||
},
|
||||
"path": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
@@ -524,25 +467,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref common.Refere
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"encryptedToken": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
Type: []string{"string"},
|
||||
Format: "byte",
|
||||
},
|
||||
},
|
||||
"path": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
@@ -2378,8 +2302,7 @@ func schema_pkg_apis_provisioning_v0alpha1_SecureValues(ref common.ReferenceCall
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "NOT YET USED FOR REAL -- testing secure value workflow",
|
||||
Type: []string{"object"},
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
@@ -2390,7 +2313,7 @@ func schema_pkg_apis_provisioning_v0alpha1_SecureValues(ref common.ReferenceCall
|
||||
},
|
||||
"webhookSecret": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Some webhooks (github) require a secret key value",
|
||||
Description: "Some webhooks (including github) require a secret key value",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"),
|
||||
},
|
||||
@@ -2671,18 +2594,6 @@ func schema_pkg_apis_provisioning_v0alpha1_WebhookStatus(ref common.ReferenceCal
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"secret": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"encryptedSecret": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "byte",
|
||||
},
|
||||
},
|
||||
"subscribedEvents": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
|
||||
+4
-24
@@ -7,12 +7,10 @@ package v0alpha1
|
||||
// BitbucketRepositoryConfigApplyConfiguration represents a declarative configuration of the BitbucketRepositoryConfig type for use
|
||||
// with apply.
|
||||
type BitbucketRepositoryConfigApplyConfiguration struct {
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
TokenUser *string `json:"tokenUser,omitempty"`
|
||||
Token *string `json:"token,omitempty"`
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
TokenUser *string `json:"tokenUser,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// BitbucketRepositoryConfigApplyConfiguration constructs a declarative configuration of the BitbucketRepositoryConfig type for use with
|
||||
@@ -45,24 +43,6 @@ func (b *BitbucketRepositoryConfigApplyConfiguration) WithTokenUser(value string
|
||||
return b
|
||||
}
|
||||
|
||||
// WithToken sets the Token field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Token field is set to the value of the last call.
|
||||
func (b *BitbucketRepositoryConfigApplyConfiguration) WithToken(value string) *BitbucketRepositoryConfigApplyConfiguration {
|
||||
b.Token = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the EncryptedToken field.
|
||||
func (b *BitbucketRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *BitbucketRepositoryConfigApplyConfiguration {
|
||||
for i := range values {
|
||||
b.EncryptedToken = append(b.EncryptedToken, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPath sets the Path field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Path field is set to the value of the last call.
|
||||
|
||||
-20
@@ -9,8 +9,6 @@ package v0alpha1
|
||||
type GitHubRepositoryConfigApplyConfiguration struct {
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
Token *string `json:"token,omitempty"`
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
GenerateDashboardPreviews *bool `json:"generateDashboardPreviews,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
@@ -37,24 +35,6 @@ func (b *GitHubRepositoryConfigApplyConfiguration) WithBranch(value string) *Git
|
||||
return b
|
||||
}
|
||||
|
||||
// WithToken sets the Token field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Token field is set to the value of the last call.
|
||||
func (b *GitHubRepositoryConfigApplyConfiguration) WithToken(value string) *GitHubRepositoryConfigApplyConfiguration {
|
||||
b.Token = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the EncryptedToken field.
|
||||
func (b *GitHubRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitHubRepositoryConfigApplyConfiguration {
|
||||
for i := range values {
|
||||
b.EncryptedToken = append(b.EncryptedToken, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithGenerateDashboardPreviews sets the GenerateDashboardPreviews field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the GenerateDashboardPreviews field is set to the value of the last call.
|
||||
|
||||
+3
-23
@@ -7,11 +7,9 @@ package v0alpha1
|
||||
// GitLabRepositoryConfigApplyConfiguration represents a declarative configuration of the GitLabRepositoryConfig type for use
|
||||
// with apply.
|
||||
type GitLabRepositoryConfigApplyConfiguration struct {
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
Token *string `json:"token,omitempty"`
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// GitLabRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitLabRepositoryConfig type for use with
|
||||
@@ -36,24 +34,6 @@ func (b *GitLabRepositoryConfigApplyConfiguration) WithBranch(value string) *Git
|
||||
return b
|
||||
}
|
||||
|
||||
// WithToken sets the Token field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Token field is set to the value of the last call.
|
||||
func (b *GitLabRepositoryConfigApplyConfiguration) WithToken(value string) *GitLabRepositoryConfigApplyConfiguration {
|
||||
b.Token = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the EncryptedToken field.
|
||||
func (b *GitLabRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitLabRepositoryConfigApplyConfiguration {
|
||||
for i := range values {
|
||||
b.EncryptedToken = append(b.EncryptedToken, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPath sets the Path field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Path field is set to the value of the last call.
|
||||
|
||||
+4
-24
@@ -7,12 +7,10 @@ package v0alpha1
|
||||
// GitRepositoryConfigApplyConfiguration represents a declarative configuration of the GitRepositoryConfig type for use
|
||||
// with apply.
|
||||
type GitRepositoryConfigApplyConfiguration struct {
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
TokenUser *string `json:"tokenUser,omitempty"`
|
||||
Token *string `json:"token,omitempty"`
|
||||
EncryptedToken []byte `json:"encryptedToken,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
TokenUser *string `json:"tokenUser,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// GitRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitRepositoryConfig type for use with
|
||||
@@ -45,24 +43,6 @@ func (b *GitRepositoryConfigApplyConfiguration) WithTokenUser(value string) *Git
|
||||
return b
|
||||
}
|
||||
|
||||
// WithToken sets the Token field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Token field is set to the value of the last call.
|
||||
func (b *GitRepositoryConfigApplyConfiguration) WithToken(value string) *GitRepositoryConfigApplyConfiguration {
|
||||
b.Token = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the EncryptedToken field.
|
||||
func (b *GitRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitRepositoryConfigApplyConfiguration {
|
||||
for i := range values {
|
||||
b.EncryptedToken = append(b.EncryptedToken, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPath sets the Path field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Path field is set to the value of the last call.
|
||||
|
||||
-20
@@ -9,8 +9,6 @@ package v0alpha1
|
||||
type WebhookStatusApplyConfiguration struct {
|
||||
ID *int64 `json:"id,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Secret *string `json:"secret,omitempty"`
|
||||
EncryptedSecret []byte `json:"encryptedSecret,omitempty"`
|
||||
SubscribedEvents []string `json:"subscribedEvents,omitempty"`
|
||||
LastEvent *int64 `json:"lastEvent,omitempty"`
|
||||
}
|
||||
@@ -37,24 +35,6 @@ func (b *WebhookStatusApplyConfiguration) WithURL(value string) *WebhookStatusAp
|
||||
return b
|
||||
}
|
||||
|
||||
// WithSecret sets the Secret field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Secret field is set to the value of the last call.
|
||||
func (b *WebhookStatusApplyConfiguration) WithSecret(value string) *WebhookStatusApplyConfiguration {
|
||||
b.Secret = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithEncryptedSecret adds the given value to the EncryptedSecret field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the EncryptedSecret field.
|
||||
func (b *WebhookStatusApplyConfiguration) WithEncryptedSecret(values ...byte) *WebhookStatusApplyConfiguration {
|
||||
for i := range values {
|
||||
b.EncryptedSecret = append(b.EncryptedSecret, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithSubscribedEvents adds the given value to the SubscribedEvents field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the SubscribedEvents field.
|
||||
|
||||
@@ -302,7 +302,6 @@ require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
|
||||
+84
@@ -685,6 +685,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/bsm/sarama-cluster v2.1.13+incompatible h1:bqU3gMJbWZVxLZ9PGWVKP05yOmFXUlfw61RBwuE3PYU=
|
||||
github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM=
|
||||
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
|
||||
@@ -762,6 +764,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
|
||||
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
|
||||
github.com/cristalhq/hedgedhttp v0.9.1 h1:g68L9cf8uUyQKQJwciD0A1Vgbsz+QgCjuB1I8FAsCDs=
|
||||
github.com/cristalhq/hedgedhttp v0.9.1/go.mod h1:XkqWU6qVMutbhW68NnzjWrGtH8NUx1UfYqGYtHVKIsI=
|
||||
github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b h1:WR1qVJzbvrVywhAk4kMQKRPx09AZVI0NdEdYs59iHcA=
|
||||
github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b/go.mod h1:v9FBN7gdVTpiD/+LZ7Po0UKvROyT87uLVxTHVky/dlQ=
|
||||
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
|
||||
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
|
||||
github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo=
|
||||
@@ -826,6 +830,10 @@ github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ=
|
||||
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.6.1/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.17.1 h1:bOXChDoCMB4TIwwGqKd031U8OXssmWLT3UrAr9EGs3Q=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.17.1/go.mod h1:MVJCtL+gJJ7x5jFeUmA20O7rvipX8GcQmo5iBcmaJn4=
|
||||
github.com/elastic/go-grok v0.3.1 h1:WEhUxe2KrwycMnlvMimJXvzRa7DoByJB4PVUIE1ZD/U=
|
||||
github.com/elastic/go-grok v0.3.1/go.mod h1:n38ls8ZgOboZRgKcjMY8eFeZFMmcL9n2lP0iHhIDk64=
|
||||
github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0=
|
||||
@@ -914,6 +922,8 @@ github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
|
||||
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
|
||||
github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7 h1:TvUE5vjfoa7fFHMlmGOk0CsauNj1w4yJjR9+/GnWVCw=
|
||||
github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
|
||||
github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
|
||||
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
|
||||
github.com/gocraft/dbr/v2 v2.7.2 h1:ccUxMuz6RdZvD7VPhMRRMSS/ECF3gytPhPtcavjktHk=
|
||||
github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdNu6YJrg=
|
||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||
@@ -1172,6 +1182,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b h1:11UHH39z1RhZ5dc4y4r/4koJo6IYFgTRMe/LlwRTEw0=
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg=
|
||||
github.com/lightstep/go-expohisto v1.0.0 h1:UPtTS1rGdtehbbAF7o/dhkWLTDI73UifG8LbfQI7cA4=
|
||||
github.com/lightstep/go-expohisto v1.0.0/go.mod h1:xDXD0++Mu2FOaItXtdDfksfgxfV0z1TMPa+e/EUd0cs=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA=
|
||||
@@ -1239,11 +1251,25 @@ github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItc
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
|
||||
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
|
||||
github.com/olivere/elastic v6.2.37+incompatible h1:UfSGJem5czY+x/LqxgeCBgjDn6St+z8OnsCuxwD3L0U=
|
||||
github.com/olivere/elastic v6.2.37+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.121.0 h1:gX7HGoRE0OTMS8ZVh/zPeQe+ZEASKxAo6Wn9dIcsgRE=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.121.0/go.mod h1:y6UqtUREKcyDzLPQC5wSPbOlZR2HLUrNp/2ITZalVKA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.121.0 h1:1J3XbT944dDqif4TINht6SBz1l+HXpOeyZglaUfdY+0=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.121.0/go.mod h1:60Jb5QNQwPeb2OKfVi0M0u0bSqmQiQU/suZQD8lOMZo=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.121.0 h1:t4auPAdvyl437eQQ6P0SzZRn55/d439WK9268p3bJdc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.121.0/go.mod h1:fFtAEjQygwWnabKV++P/OqokQ8YlotrTWQFdT5hcc4M=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1 h1:+aiMrDR6xiaDM7xN4ByrBYI0Craqt68nZicmpYpt0co=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1/go.mod h1:H/TEWN4jgExt0McrtrBK2VFK6r9LRsWtqhEZrH690rs=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.121.0 h1:zI2FKKFUnzzfzgKODlMdKyeOZx+kWQROE2OP1pttZPQ=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.121.0/go.mod h1:Dc3frbQ2ZyxxpVx95mCp7ZPq+B6QzWWmEwmuT6t28Pk=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.121.0 h1:OsPz8JT40DbRB4W3j5ypEgLQyjxFN9kCw1xNrmyzgCs=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.121.0/go.mod h1:/+tb7Hkt0i92WFkkfHurT7rXrZ98nxat5AeGmbBYFm4=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121.0 h1:Q27uarV8CEGLKfG4OJQGFkOKZ49/SDEqTr6BbMOoKmA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121.0/go.mod h1:f9MPSdMvooe2s9Zg0WVBJOvg72d9fFikxFt7yniFros=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1/go.mod h1:AFMryJmht7dZxcAwc2sx/r4gxbriElWw49ugxKp2mcA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.121.0 h1:I+F6xdXQsiXXdce7yjHN+y4LX5MrZI1kNmhBunJffdA=
|
||||
@@ -1252,26 +1278,46 @@ github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.124
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.124.1/go.mod h1:VqJ9CWEwk6N7YzumaV1gbxY0UeLlnzAzYmTw8ieaIYs=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.124.1 h1:hbJs+dkXx3bPshr614bYk6bwvtM9YruaGqhwXWP2UuU=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.124.1/go.mod h1:oDdNpMFQ6hT0IsGDRHkG5mQ3DLxF6kYvDuoEIcoQbCo=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.121.0 h1:AY0metK/HpBqnqv6H8jgY7JTEXnc7cNdKWYZsDArXJE=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.121.0/go.mod h1:xVMTbBp/I+Y0j1hE05Vm4mk32t80Z4Ee8/rybGjYDM0=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.124.1 h1:QUXMxZTjTER4vDa4ldkSSAd89blgS5974j5AkRLp2M0=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.124.1/go.mod h1:jRQpw8VJDmUmyH8ONQ5FmhYEXkaxzMFX6Tei8N4GDZs=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0 h1:dnRf9xyjTBGHui949SS0A87fNa49Al32dog9XmeGY7I=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0/go.mod h1:ZAFQ1Te56fodNun+lmC/jFjVxhecumV882ct8sbdeAM=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 h1:E1e96GTHmiAfIfeYfA5ZVnOxud3+vbisGp0gE1tfd4s=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1/go.mod h1:MOhFATtYSLad9nKunjh6uGf8nQUcWje2LPlhD2uu3do=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1 h1:QX88VjAiUaG7SQe4A14b3JPgvzZK6OXpWoBd32Zlp1o=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1/go.mod h1:jRsmTRbn1HkrOpoeXS0KGfXobrh7E3W/p9fLwLdpcB8=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.121.0 h1:7xzxbwIBpb8LlMNY+RIdEnWTJH+pDu0LiL+tcIziEkA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.121.0/go.mod h1:gePUJjHVTT0N6ruTBSA3vDlDlh+TJ4tGt/mm7o1MIuQ=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.124.1 h1:oiLa2lg+Ix+wHi4bPsnQ5+DJc6u+OHZt6YhP/rjgbNc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.124.1/go.mod h1:v8WbCvCAzp8OgxdyXSZlLUkEKJJOLhlOF57kRCVUoAQ=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.124.1 h1:G2daAIXiQhAwQSz9RK71QsBH9rmH/m/vdkFuGIEPfS4=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.124.1/go.mod h1:/WAA1PKvHNz7E5SrtGg2KfAWl/PrmS0FVYOanoGxk0I=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.124.1 h1:mMVzpkpy6rKL1Q/xXNogZVtWebIlxTRzhsgp3b9ioCM=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.124.1/go.mod h1:jM8Gsd0fIiwRzWrzd7Gm6PZYi5AgHPRkz0625Rtqyxo=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.121.0 h1:9cNRnGUjm9jh8zPyMRxTSLIqP+mpY2KVJSjwq4sGJIA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.121.0/go.mod h1:JjmyxvVh1wwMNnN+KXYUZGNkU/L779q8Yb7lNsB4KSk=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0 h1:Gv2xDvT8r/zH7MzGRcMO4ckmP1UpNNbEfN7fKx3vqUQ=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0/go.mod h1:PQjfR4WB9Q1MR9X/6IqxWGpKfavp9OF1fUcAqnPGGpQ=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1 h1:kPPjaOlncaJjoaJRUzW3TbqHM4v/+TtxC2Bzpa7z9X4=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1/go.mod h1:+6zqsZ1YGNsl7RUxfjzg7MKEWd5J6QAlq7zM3Nqy8UI=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 h1:9c6L4xlAMqhOg5y54Bc2B5t0i49yz7v2I6I8RY4Z0/o=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1/go.mod h1:6f0N58o0cOHC0ApSM/qrooVmQza1eQ7L53PDE91uO1Q=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1 h1:IYxc8uPVCtKlgxjbYFGZ5/wXLdwAH1WQPKRlzaXtr8E=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1/go.mod h1:jFRiSEn2ss3e4CWG6GTDOJMYRI0C/N2XbMMIBzZuqZY=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.121.0 h1:QzV771eoAx2W9HCFq/YJVMNgEuG4lvalPTQIutheFhc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.121.0/go.mod h1:+HFLuxyyP0bSsnx/L7ATAukEPQ8+05kcOps8mJeYPb8=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.124.1 h1:cyvsrj/D+/XwoXSw/4FD/S1M50jco7k963f/+si907w=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.124.1/go.mod h1:xAU/ievEszZYP1DGWy+yYjAjfjRCKyBLBAnXYvKWNWc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.121.0 h1:/d5mamybZt/HWFUhn9khuHf9aDYO6UFmy56Kz65FD4c=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.121.0/go.mod h1:C1b83q4Wf1D2TYaX6jrwJPJsJhOU6igzMhcWqVz1d7c=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.121.0 h1:+wj+Sw08WDdL/9lD4OUy1PFgQMsiyLuSmlmb3HbKPv4=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.121.0/go.mod h1:YczZl2MmjOUdg5eXg+fAW0my/EG+77b27ue6vj7xPHU=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.124.1 h1:qkqiqLwfg7hj+oDYvpmMD64p+poaxXwo654ZE44uPm4=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.124.1/go.mod h1:B/GP3l4Y1qNsNtWVIzpwS8jWB1Nn/vx0sFBlVDkWt9E=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.121.0 h1:CDMMmOdUF3EPvhNFBMfPrSMvqBp97FeATs/kdVnqHGc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.121.0/go.mod h1:qvnIa2vzFlLKsILRWHKubjHIWfWWRAiNZtXdaa/oSEc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.124.1 h1:r20zOMMBcxzI2Sni2HyTR7TVxuNXLn1Ov6qtnlVWoJc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.124.1/go.mod h1:1UsKa4xSNodA2i6ic5mmyd31khXyTGxsVKJoKmq/jtU=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.124.1 h1:Pdlf5D5gB/5Khi3t5PiojQrCulcEejecsxoSpOd0ekA=
|
||||
@@ -1300,6 +1346,8 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
|
||||
github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
|
||||
@@ -1342,6 +1390,8 @@ github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNl
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY=
|
||||
github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk=
|
||||
github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ=
|
||||
@@ -1389,6 +1439,8 @@ github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8b
|
||||
github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
|
||||
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
|
||||
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0=
|
||||
github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0 h1:gUZ25e1DVE/0+ZZ0nupsIo+C1j7UNloN7Pkg3w6tceI=
|
||||
github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0/go.mod h1:2Fc67EpyOEexLAF99zhSuzu9H22zd83pkjxEHHTtHf4=
|
||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0 h1:i1Kh9fmXgHG9z3uzJv5Arz7pDKVaaNpLrqyd+0xhYMA=
|
||||
@@ -1509,6 +1561,8 @@ go.opentelemetry.io/collector/config/configgrpc v0.124.0 h1:aTuHYsyLMaGnd0o39Qy9
|
||||
go.opentelemetry.io/collector/config/configgrpc v0.124.0/go.mod h1:EEC4T+hCfXbbC+711GaVFuJNBU7hdYR4Vya4RWu7xc0=
|
||||
go.opentelemetry.io/collector/config/confighttp v0.124.0 h1:W75DaPeLUuGbJtX3cTXOK0b53S5zrUsh6g5UfB6Wzsw=
|
||||
go.opentelemetry.io/collector/config/confighttp v0.124.0/go.mod h1:hiTu8HFgnzSitrogLz1urQn/+FzNzarqYk4BICy/ABs=
|
||||
go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.121.0 h1:UpYVKYPvIxc7iekPYJ0xjN9jDFCdDIAkcT9WsRN6syg=
|
||||
go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.121.0/go.mod h1:a5C/yjslp2GsNoA/OBBq7lYy2W3ie/drjNjfBtDgQlk=
|
||||
go.opentelemetry.io/collector/config/confignet v1.30.0 h1:2axBhT7xKpCUFgU6KokrDnG9cjtw7gXlACP1uVCUK0s=
|
||||
go.opentelemetry.io/collector/config/confignet v1.30.0/go.mod h1:HgpLwdRLzPTwbjpUXR0Wdt6pAHuYzaIr8t4yECKrEvo=
|
||||
go.opentelemetry.io/collector/config/configopaque v1.30.0 h1:vR2UxmzLwmkmQwyh16w8MyLODKdpNVKh0L3JFOZKzQ8=
|
||||
@@ -1521,12 +1575,24 @@ go.opentelemetry.io/collector/config/configtls v1.30.0 h1:wLTRV5hn/FWKWNjZ/9/ckk
|
||||
go.opentelemetry.io/collector/config/configtls v1.30.0/go.mod h1:yCM4ZYkLvc1VjpT/1DQIVoGmzEBHOhZltYQ7A30BMyM=
|
||||
go.opentelemetry.io/collector/confmap v1.30.0 h1:Y0MXhjQCdMyJN9xZMWWdNPWs6ncMVf7YVnyAEN2dAcM=
|
||||
go.opentelemetry.io/collector/confmap v1.30.0/go.mod h1:9DdThVDIC3VsdtTb7DgT+HwusWOocoqDkd/TErEtQgA=
|
||||
go.opentelemetry.io/collector/confmap/provider/envprovider v1.27.0 h1:zozJGcpzvv9DqgS9K8Ln5xQluPk2xUfvCVwJ4rifWqs=
|
||||
go.opentelemetry.io/collector/confmap/provider/envprovider v1.27.0/go.mod h1:skFxNgX9R39cP4FqjSPKIc/F3nDK97bsqwdCfZ9wpgs=
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.27.0 h1:T6eWNB7OFD9E1xPo8XsGGLgkTC7a7pe4Tu2iFPWSwP8=
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.27.0/go.mod h1:jcDGh02SI9vv8OqNRenqpffWlCC8bYqqFVsfIbdAG7c=
|
||||
go.opentelemetry.io/collector/confmap/provider/httpprovider v1.27.0 h1:u144bOgB+SgCcm41OpN1PflAbGEMsHRMMa/lMIRpm/I=
|
||||
go.opentelemetry.io/collector/confmap/provider/httpprovider v1.27.0/go.mod h1:dO/PLYcLJJmF2upHutaLRuW9Jf0ImqdIMHxr5aGiXro=
|
||||
go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.27.0 h1:sfKbgYXag055VFd6qau+7A2JF7Igbbfw7GxKToLLPK0=
|
||||
go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.27.0/go.mod h1:Nbpvr2DPGQewh9lC/qD0O+iOetDG3JWHqeKcYESCKrM=
|
||||
go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.27.0 h1:X7fohJKfGCS1cwY6/wmcjNmT/KGgcJfkyzBNnyNYiPQ=
|
||||
go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.27.0/go.mod h1:/vvb4mYN+J4IA2hRlkqAiBdxyOJFamW60ChxqDi8o1Y=
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.124.0 h1:PK+CaSgjLvzHaafBieJ3AjiUTAPuf40C+/Fn38LvmW8=
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.124.0/go.mod h1:DZmFSgWiqXQrzld9uU+73YAVI5JRIgd8RkK5HcaXGU0=
|
||||
go.opentelemetry.io/collector/connector v0.124.0 h1:/Wk8A4gOqjhE+WvKCMqCFhzUIvSi3sdN3RGvopjD6SY=
|
||||
go.opentelemetry.io/collector/connector v0.124.0/go.mod h1:dnYcXgUZp8ZmT7nbBPf38+mP2DD3T47m9jyGbdaCEXc=
|
||||
go.opentelemetry.io/collector/connector/connectortest v0.124.0 h1:gAD2jt7Th6DD8tDTU72Sv2xXvqJEGSjfncr9nTSVCg8=
|
||||
go.opentelemetry.io/collector/connector/connectortest v0.124.0/go.mod h1:0017vT2aCY1NmYXEepxvEfMA9YufKUoBM3/qtD6k9UM=
|
||||
go.opentelemetry.io/collector/connector/forwardconnector v0.121.0 h1:AJtudkGK2eCp/jwoJAG5mN79+HOdthnZOUUynzUhzqc=
|
||||
go.opentelemetry.io/collector/connector/forwardconnector v0.121.0/go.mod h1:IntxKXr4nuDq0GjGsPpz+gO9J9ZIwJrDdHIZnpmkZGI=
|
||||
go.opentelemetry.io/collector/connector/xconnector v0.124.0 h1:rdjwSfajHjJVRznw/NKGGzY0PKBTKBypZngGxOaJuEg=
|
||||
go.opentelemetry.io/collector/connector/xconnector v0.124.0/go.mod h1:rOhdUXPzTZbJ2L8VV43r7Rz/ZBfgWxQ+RI9mcqlzz5g=
|
||||
go.opentelemetry.io/collector/consumer v1.30.0 h1:Nn6kFTH+EJbv13E0W+sNvWrTgbiFCRv8f6DaA2F1DQs=
|
||||
@@ -1541,10 +1607,14 @@ go.opentelemetry.io/collector/consumer/xconsumer v0.124.0 h1:/cut96EWVNoz6lIeGI9
|
||||
go.opentelemetry.io/collector/consumer/xconsumer v0.124.0/go.mod h1:fHH/MpzFCRNk/4foiYE6BoXQCAMf5sJTO35uvzVrrd4=
|
||||
go.opentelemetry.io/collector/exporter v0.124.0 h1:ii+9tU/iSrPl4+YDvqFVflksA9hUYEzwMIpmvP4JZ8w=
|
||||
go.opentelemetry.io/collector/exporter v0.124.0/go.mod h1:Q8tOEwFu3CN8VGjE4H2yZcCRG9Q60foQIyZGKPD/jig=
|
||||
go.opentelemetry.io/collector/exporter/debugexporter v0.121.0 h1:x9KJRTTEGPu7DxYNFC5LvP0KjrxCTS8uD6Zprn/gTWM=
|
||||
go.opentelemetry.io/collector/exporter/debugexporter v0.121.0/go.mod h1:90WFQRnuMryVAlQ9a5jIuh6yYsLg+K8P5uHG/z4Zyhg=
|
||||
go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.124.0 h1:rU8CkyMIkLnjQAM6Yjd/a2gOQ/Svsjd/8jCNKEmrStw=
|
||||
go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.124.0/go.mod h1:2dO+z9QWGZwyxGOVLuCKAT0n+da4FMIaRjQDXdhcFyM=
|
||||
go.opentelemetry.io/collector/exporter/exportertest v0.124.0 h1:IOxA/4CiVWGPlmA0JofK6W4DzvwW1YJes09r6osluIE=
|
||||
go.opentelemetry.io/collector/exporter/exportertest v0.124.0/go.mod h1:2EmU8IwVJV79MmFBFFW1LCN0Ob2UZsEkX/mSUB06lbI=
|
||||
go.opentelemetry.io/collector/exporter/nopexporter v0.121.0 h1:sgEtYYqzrqLtyhRr1afnmcOG3Il2i6soY7GdYhqrCgw=
|
||||
go.opentelemetry.io/collector/exporter/nopexporter v0.121.0/go.mod h1:KHLzB4ahpYu1YD4RPnhEArzh0L7/NtV1W2U4bzKH/Xo=
|
||||
go.opentelemetry.io/collector/exporter/otlpexporter v0.124.0 h1:wbOnCi01UT9YGkK86Jl13DEBTCdgIXSLmX8RajQ66LM=
|
||||
go.opentelemetry.io/collector/exporter/otlpexporter v0.124.0/go.mod h1:zzugHvRuxWsl8+T2Dj61QAfk7CSWhl26jp0y5LNPVig=
|
||||
go.opentelemetry.io/collector/exporter/otlphttpexporter v0.124.0 h1:047D4wLOb5ug4O99y3AjR/SNFTxlq3RAr2zr/8G1JWI=
|
||||
@@ -1561,10 +1631,14 @@ go.opentelemetry.io/collector/extension/extensiontest v0.124.0 h1:pWfKxEqvq5vVdQ
|
||||
go.opentelemetry.io/collector/extension/extensiontest v0.124.0/go.mod h1:DLVRyW7tJt8TtYq0Wr5BUsM494YqDiIjN8YCmbVKqjs=
|
||||
go.opentelemetry.io/collector/extension/xextension v0.124.0 h1:Yzf11HXaiMHfS50Zy/CYKfJjoi+/w/tgRZdDQ2VIdW0=
|
||||
go.opentelemetry.io/collector/extension/xextension v0.124.0/go.mod h1:GeM0aSgwVSba3Bvvspuy1E+1aa/Q1CDxoK+e/xcJFVg=
|
||||
go.opentelemetry.io/collector/extension/zpagesextension v0.121.0 h1:zCnIPyZwHkqq33MRROQN2JTKxpLadTq8ppiR1x9rbOM=
|
||||
go.opentelemetry.io/collector/extension/zpagesextension v0.121.0/go.mod h1:W2ZcPYdyN7ux7AD5fA0/YzW8EA2aOpUSia/YoDsh7ck=
|
||||
go.opentelemetry.io/collector/featuregate v1.30.0 h1:mx7+iP/FQnY7KO8qw/xE3Qd1MQkWcU8VgcqLNrJ8EU8=
|
||||
go.opentelemetry.io/collector/featuregate v1.30.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc=
|
||||
go.opentelemetry.io/collector/internal/fanoutconsumer v0.124.0 h1:8+xc3OxriK1nZNBApFCzF7lszXyBQxyJ/Nnzy5Q4hCM=
|
||||
go.opentelemetry.io/collector/internal/fanoutconsumer v0.124.0/go.mod h1:CoT5fVYpTT4RWUE9DihSMlxXqGP/VnILnBBGld8Bu6o=
|
||||
go.opentelemetry.io/collector/internal/memorylimiter v0.121.0 h1:0QZGL5zwaHMAJqUVfyRtXNCJWhG47eyDBoRNSeQvEEc=
|
||||
go.opentelemetry.io/collector/internal/memorylimiter v0.121.0/go.mod h1:Be9ZYdHt0YibCEkw/W2v0jcEA8h3bLb/cmUW5gWg+QI=
|
||||
go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0 h1:HZTic8bbD86FFxE9xw8ZXLBCIwGObL9m70REo+64ZmY=
|
||||
go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0/go.mod h1:yQs/QFRYvAS2ak2mh9CXblFGbFEPLJ7FaFc8rTpL8C0=
|
||||
go.opentelemetry.io/collector/internal/telemetry v0.124.0 h1:kzd1/ZYhLj4bt2pDB529mL4rIRrRacemXodFNxfhdWk=
|
||||
@@ -1581,14 +1655,22 @@ go.opentelemetry.io/collector/pipeline/xpipeline v0.124.0 h1:ADHUrozlIgSDjXMsAC5
|
||||
go.opentelemetry.io/collector/pipeline/xpipeline v0.124.0/go.mod h1:ep7XJFdCEq04/5yUyiWWzgKvBYMwRJR5XNWmGpIGbVQ=
|
||||
go.opentelemetry.io/collector/processor v1.30.0 h1:dxmu+sO6MzQydyrf2CON5Hm1KU7yV4ofH1stmreUtPk=
|
||||
go.opentelemetry.io/collector/processor v1.30.0/go.mod h1:DjXAgelT8rfIWCTJP5kiPpxPqz4JLE1mJwsE2kJMTk8=
|
||||
go.opentelemetry.io/collector/processor/batchprocessor v0.121.0 h1:dKE9lMhaQgg0VIU3/cVKPsPz8K8JXpE752C+iFDop8k=
|
||||
go.opentelemetry.io/collector/processor/batchprocessor v0.121.0/go.mod h1:dk7ngHltf0n4J2ec/PjwXAS9K8176Cpkx78N4IbAm04=
|
||||
go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.121.0 h1:GnBF8QLD3Eeje4tNxtdWERZmIgPc06jiYe3FL1jk/yQ=
|
||||
go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.121.0/go.mod h1:zCUCi5xPklccgvKAmSalTwMNBIi83vnkIZVTGuG0pz0=
|
||||
go.opentelemetry.io/collector/processor/processorhelper v0.124.0 h1:zIBpPn/88FVusy/WL+k0jKNVEX+cRnnPhKNc+B9TRds=
|
||||
go.opentelemetry.io/collector/processor/processorhelper v0.124.0/go.mod h1:NNy1C10OSGSV9nUl+YXYBETYxNacGPYM1PHFb+j0Su4=
|
||||
go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.121.0 h1:O4CzvJCV1soQOoHSew+FEGhbhXWPxJGB7pYYkAhdEUU=
|
||||
go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.121.0/go.mod h1:CUuVIwN4uAJzuAEr3hYaQXmc865eS37eJl5jQ2LgbWc=
|
||||
go.opentelemetry.io/collector/processor/processortest v0.124.0 h1:qcyo0dSWmgpNFxjObsKk3Rd/wWV8CkMevd+jApkTQWE=
|
||||
go.opentelemetry.io/collector/processor/processortest v0.124.0/go.mod h1:1YDTxd4c/uVU3Ui1+AzvYW94mo5DbhNmB1xSof6zvD0=
|
||||
go.opentelemetry.io/collector/processor/xprocessor v0.124.0 h1:KAe8gIje8TcB8varZ4PDy0HV5xX5rNdaQ7q46BE915w=
|
||||
go.opentelemetry.io/collector/processor/xprocessor v0.124.0/go.mod h1:ItJBBlR6/141vg1v4iRrcsBrGjPCgmXAztxS2x2YkdI=
|
||||
go.opentelemetry.io/collector/receiver v1.30.0 h1:XbgU4yT3Ld+hL9+jHcD/Kctcr3gXjpiFxKO+50pSayg=
|
||||
go.opentelemetry.io/collector/receiver v1.30.0/go.mod h1:U3cApz9PHiRMgN0WkZaz4o8mvj1+cVQYsyj2Nl1v3FQ=
|
||||
go.opentelemetry.io/collector/receiver/nopreceiver v0.121.0 h1:fAlBC6JXyKnd6OY0oK0pk1ROc/VfA0OonrPOixXBZqQ=
|
||||
go.opentelemetry.io/collector/receiver/nopreceiver v0.121.0/go.mod h1:iyAp4FFiNCkERuFnFhHNbhNiVrk04pEZJQTEw5KLMMA=
|
||||
go.opentelemetry.io/collector/receiver/otlpreceiver v0.124.0 h1:DwvPyh6X0ytmD0IIjOq62ui3Bg5FwkaX6DzseGg62sY=
|
||||
go.opentelemetry.io/collector/receiver/otlpreceiver v0.124.0/go.mod h1:QG2c9RW/Ys1L30cGH24RBJk72H27UdB50YmpY2rclNs=
|
||||
go.opentelemetry.io/collector/receiver/receiverhelper v0.124.0 h1:lOCWpiZUzeXV0zDN4jzwqgD6gFdhsY9Mp1om5XzrWKk=
|
||||
@@ -1605,6 +1687,8 @@ go.opentelemetry.io/collector/service/hostcapabilities v0.124.0 h1:ArxbARF7+bnzK
|
||||
go.opentelemetry.io/collector/service/hostcapabilities v0.124.0/go.mod h1:vifQsB+lkeCsjBCRPVHca9lJ3pLpLPZKCGrG77nkxFQ=
|
||||
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU=
|
||||
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0=
|
||||
go.opentelemetry.io/contrib/config v0.14.0 h1:QAG8uHNp5ZiCkpT7XggSmg5AyW1sA0LgypMoXgBB1+4=
|
||||
go.opentelemetry.io/contrib/config v0.14.0/go.mod h1:77rDmFPqBae5jtQ2C78RuDTHz4P27C8LzoN0MZyumYQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
|
||||
|
||||
@@ -197,10 +197,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
provisioning?: boolean;
|
||||
/**
|
||||
* Experimental feature to use the secrets service for provisioning instead of the legacy secrets
|
||||
*/
|
||||
provisioningSecretsService?: boolean;
|
||||
/**
|
||||
* Start an additional https handler and write kubectl options
|
||||
*/
|
||||
grafanaAPIServerEnsureKubectlAccess?: boolean;
|
||||
|
||||
@@ -31,7 +31,7 @@ type RepoGetter interface {
|
||||
// Given a repository configuration, return it as a repository instance
|
||||
// This will only error for un-recoverable system errors
|
||||
// the repository instance may or may not be valid/healthy
|
||||
AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
RepositoryFromConfig(ctx context.Context, r *provisioning.Repository) (repository.Repository, error)
|
||||
}
|
||||
|
||||
const loggerName = "provisioning-repository-controller"
|
||||
@@ -223,7 +223,7 @@ func (rc *RepositoryController) handleDelete(ctx context.Context, obj *provision
|
||||
|
||||
// Process any finalizers
|
||||
if len(obj.Finalizers) > 0 {
|
||||
repo, err := rc.repoGetter.AsRepository(ctx, obj)
|
||||
repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj)
|
||||
if err != nil {
|
||||
logger.Warn("unable to get repository for cleanup")
|
||||
} else {
|
||||
@@ -438,7 +438,7 @@ func (rc *RepositoryController) process(item *queueItem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := rc.repoGetter.AsRepository(ctx, obj)
|
||||
repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create repository from configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ package provisioning
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
type Extra interface {
|
||||
@@ -17,7 +18,7 @@ type Extra interface {
|
||||
UpdateStorage(storage map[string]rest.Storage) error
|
||||
PostProcessOpenAPI(oas *spec3.OpenAPI) error
|
||||
GetJobWorkers() []jobs.Worker
|
||||
AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error)
|
||||
AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error)
|
||||
RepositoryTypes() []provisioning.RepositoryType
|
||||
Mutators() []controller.Mutator
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
|
||||
informers "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions"
|
||||
listers "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1"
|
||||
commonMeta "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
apiutils "github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apiserver/readonly"
|
||||
@@ -56,7 +57,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/usage"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@@ -98,20 +98,19 @@ type APIBuilder struct {
|
||||
jobs.Queue
|
||||
jobs.Store
|
||||
}
|
||||
jobHistoryConfig *JobHistoryConfig
|
||||
jobHistoryLoki *jobs.LokiJobHistory
|
||||
resourceLister resources.ResourceLister
|
||||
repositoryLister listers.RepositoryLister
|
||||
legacyMigrator legacy.LegacyMigrator
|
||||
storageStatus dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
decryptSvc secret.DecryptService
|
||||
repositorySecrets secrets.RepositorySecrets // << Will be removed when the decryptSvc usage is stable
|
||||
client client.ProvisioningV0alpha1Interface
|
||||
access authlib.AccessChecker
|
||||
mutators []controller.Mutator
|
||||
statusPatcher *controller.RepositoryStatusPatcher
|
||||
healthChecker *controller.HealthChecker
|
||||
jobHistoryConfig *JobHistoryConfig
|
||||
jobHistoryLoki *jobs.LokiJobHistory
|
||||
resourceLister resources.ResourceLister
|
||||
repositoryLister listers.RepositoryLister
|
||||
legacyMigrator legacy.LegacyMigrator
|
||||
storageStatus dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
decrypter repository.Decrypter
|
||||
client client.ProvisioningV0alpha1Interface
|
||||
access authlib.AccessChecker
|
||||
mutators []controller.Mutator
|
||||
statusPatcher *controller.RepositoryStatusPatcher
|
||||
healthChecker *controller.HealthChecker
|
||||
// Extras provides additional functionality to the API.
|
||||
extras []Extra
|
||||
availableRepositoryTypes map[provisioning.RepositoryType]bool
|
||||
@@ -130,7 +129,6 @@ func NewAPIBuilder(
|
||||
storageStatus dualwrite.Service,
|
||||
usageStats usagestats.Service,
|
||||
decryptSvc secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
access authlib.AccessChecker,
|
||||
tracer tracing.Tracer,
|
||||
extraBuilders []ExtraBuilder,
|
||||
@@ -141,8 +139,8 @@ func NewAPIBuilder(
|
||||
resourceLister := resources.NewResourceLister(unified, unified, legacyMigrator, storageStatus)
|
||||
|
||||
mutators := []controller.Mutator{
|
||||
git.Mutator(repositorySecrets),
|
||||
github.Mutator(repositorySecrets),
|
||||
git.Mutator(),
|
||||
github.Mutator(),
|
||||
}
|
||||
|
||||
b := &APIBuilder{
|
||||
@@ -159,8 +157,7 @@ func NewAPIBuilder(
|
||||
legacyMigrator: legacyMigrator,
|
||||
storageStatus: storageStatus,
|
||||
unified: unified,
|
||||
decryptSvc: decryptSvc,
|
||||
repositorySecrets: repositorySecrets,
|
||||
decrypter: repository.DecryptService(decryptSvc),
|
||||
access: access,
|
||||
jobHistoryConfig: jobHistoryConfig,
|
||||
availableRepositoryTypes: map[provisioning.RepositoryType]bool{
|
||||
@@ -233,7 +230,6 @@ func RegisterAPIService(
|
||||
storageStatus dualwrite.Service,
|
||||
usageStats usagestats.Service,
|
||||
decryptSvc secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
tracer tracing.Tracer,
|
||||
extraBuilders []ExtraBuilder,
|
||||
) (*APIBuilder, error) {
|
||||
@@ -251,7 +247,6 @@ func RegisterAPIService(
|
||||
legacyMigrator, storageStatus,
|
||||
usageStats,
|
||||
decryptSvc,
|
||||
repositorySecrets,
|
||||
access,
|
||||
tracer,
|
||||
extraBuilders,
|
||||
@@ -562,7 +557,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := b.asRepository(ctx, obj)
|
||||
repo, err := b.asRepository(ctx, obj, a.GetOldObject())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -571,7 +566,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
|
||||
cfg := repo.Config()
|
||||
|
||||
if a.GetOperation() == admission.Update {
|
||||
oldRepo, err := b.asRepository(ctx, a.GetOldObject())
|
||||
oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get old repository for update: %w", err)
|
||||
}
|
||||
@@ -1263,7 +1258,7 @@ func (b *APIBuilder) GetRepository(ctx context.Context, name string) (repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.asRepository(ctx, obj)
|
||||
return b.asRepository(ctx, obj, nil)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (repository.Repository, error) {
|
||||
@@ -1283,7 +1278,7 @@ func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (rep
|
||||
return repo, err
|
||||
}
|
||||
|
||||
func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repository.Repository, error) {
|
||||
func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object, old runtime.Object) (repository.Repository, error) {
|
||||
if obj == nil {
|
||||
return nil, fmt.Errorf("missing repository object")
|
||||
}
|
||||
@@ -1291,13 +1286,30 @@ func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repo
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected repository configuration")
|
||||
}
|
||||
return b.AsRepository(ctx, r)
|
||||
|
||||
// Copy previous values if they exist
|
||||
if old != nil {
|
||||
o, ok := old.(*provisioning.Repository)
|
||||
if ok && !o.Secure.IsZero() {
|
||||
if r.Secure.Token.IsZero() {
|
||||
r.Secure.Token = o.Secure.Token
|
||||
}
|
||||
if r.Secure.WebhookSecret.IsZero() {
|
||||
r.Secure.WebhookSecret = o.Secure.WebhookSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b.RepositoryFromConfig(ctx, r)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
func (b *APIBuilder) RepositoryFromConfig(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
// Prepare a decrypter
|
||||
secure := b.decrypter(r)
|
||||
|
||||
// Try first with any extra
|
||||
for _, extra := range b.extras {
|
||||
r, err := extra.AsRepository(ctx, r)
|
||||
r, err := extra.AsRepository(ctx, r, secure)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert repository for extra %T: %w", extra, err)
|
||||
}
|
||||
@@ -1307,6 +1319,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
}
|
||||
}
|
||||
|
||||
var token commonMeta.RawSecureValue
|
||||
if r.Secure.Token.IsZero() {
|
||||
t, err := secure.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt token: %w", err)
|
||||
}
|
||||
token = t
|
||||
}
|
||||
|
||||
switch r.Spec.Type {
|
||||
case provisioning.BitbucketRepositoryType:
|
||||
return nil, errors.New("repository type bitbucket is not available")
|
||||
@@ -1315,26 +1336,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
case provisioning.LocalRepositoryType:
|
||||
return local.NewLocal(r, b.localFileResolver), nil
|
||||
case provisioning.GitRepositoryType:
|
||||
// Decrypt token if needed
|
||||
token := r.Spec.Git.Token
|
||||
if token == "" && len(r.Spec.Git.EncryptedToken) > 0 {
|
||||
decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(r.Spec.Git.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt git token: %w", err)
|
||||
}
|
||||
token = string(decrypted)
|
||||
}
|
||||
|
||||
cfg := git.RepositoryConfig{
|
||||
URL: r.Spec.Git.URL,
|
||||
Branch: r.Spec.Git.Branch,
|
||||
Path: r.Spec.Git.Path,
|
||||
TokenUser: r.Spec.Git.TokenUser,
|
||||
Token: token,
|
||||
EncryptedToken: r.Spec.Git.EncryptedToken,
|
||||
URL: r.Spec.Git.URL,
|
||||
Branch: r.Spec.Git.Branch,
|
||||
Path: r.Spec.Git.Path,
|
||||
TokenUser: r.Spec.Git.TokenUser,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
return git.NewGitRepository(ctx, r, cfg, b.repositorySecrets)
|
||||
return git.NewGitRepository(ctx, r, cfg)
|
||||
case provisioning.GitHubRepositoryType:
|
||||
logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path)
|
||||
logger.Info("Instantiating Github repository")
|
||||
@@ -1344,30 +1354,19 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
return nil, fmt.Errorf("github configuration is required for nano git")
|
||||
}
|
||||
|
||||
// Decrypt GitHub token if needed
|
||||
ghToken := ghCfg.Token
|
||||
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
|
||||
decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
ghToken = string(decrypted)
|
||||
}
|
||||
|
||||
gitCfg := git.RepositoryConfig{
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
EncryptedToken: ghCfg.EncryptedToken,
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, b.repositorySecrets)
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating git repository: %w", err)
|
||||
}
|
||||
|
||||
ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken, b.repositorySecrets)
|
||||
ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating github repository: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ type GitRepository interface {
|
||||
repository.Writer
|
||||
repository.Reader
|
||||
repository.StageableRepository
|
||||
repository.Hooks
|
||||
URL() string
|
||||
Branch() string
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
func Mutator() controller.Mutator {
|
||||
return func(ctx context.Context, obj runtime.Object) error {
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
@@ -40,16 +39,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
}
|
||||
}
|
||||
|
||||
if repo.Spec.Git.Token != "" {
|
||||
secretName := repo.Name + gitTokenSecretSuffix
|
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.Git.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.Spec.Git.EncryptedToken = nameOrValue
|
||||
repo.Spec.Git.Token = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,98 +2,23 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
func TestMutator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
setupMocks func(*secrets.MockRepositorySecrets)
|
||||
expectedToken string
|
||||
expectedEncryptedToken string
|
||||
expectedError string
|
||||
expectedURL string
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
expectedError string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
name: "successful token encryption",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
"secret-token",
|
||||
).Return([]byte("encrypted-token"), nil)
|
||||
},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "encrypted-token",
|
||||
},
|
||||
{
|
||||
name: "encryption error",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
"secret-token",
|
||||
).Return(nil, errors.New("encryption failed"))
|
||||
},
|
||||
expectedError: "encryption failed",
|
||||
},
|
||||
{
|
||||
name: "no git spec",
|
||||
obj: &provisioning.Repository{
|
||||
@@ -106,11 +31,7 @@ func TestMutator(t *testing.T) {
|
||||
Git: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "no git spec for git repository type",
|
||||
obj: &provisioning.Repository{
|
||||
@@ -123,9 +44,6 @@ func TestMutator(t *testing.T) {
|
||||
Git: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedError: "git configuration is required for git repository type",
|
||||
},
|
||||
{
|
||||
@@ -137,21 +55,13 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "",
|
||||
},
|
||||
Git: &provisioning.GitRepositoryConfig{},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-repository object",
|
||||
obj: &runtime.Unknown{},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URL normalization - add .git suffix",
|
||||
@@ -167,9 +77,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -186,9 +93,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -205,9 +109,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -224,9 +125,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -243,19 +141,13 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMocks(mockSecrets)
|
||||
|
||||
mutator := Mutator(mockSecrets)
|
||||
mutator := Mutator()
|
||||
err := mutator(context.Background(), tt.obj)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
@@ -266,13 +158,6 @@ func TestMutator(t *testing.T) {
|
||||
|
||||
// Check that token was cleared and encrypted token was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.Git != nil {
|
||||
if tt.expectedEncryptedToken != "" {
|
||||
// Token should be cleared after encryption
|
||||
assert.Empty(t, repo.Spec.Git.Token, "Token should be cleared after encryption")
|
||||
// EncryptedToken should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.Git.EncryptedToken), "EncryptedToken should match expected value")
|
||||
}
|
||||
|
||||
// Check URL normalization
|
||||
if tt.expectedURL != "" {
|
||||
assert.Equal(t, tt.expectedURL, repo.Spec.Git.URL, "URL should be normalized correctly")
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/nanogit"
|
||||
"github.com/grafana/nanogit/log"
|
||||
"github.com/grafana/nanogit/options"
|
||||
@@ -27,16 +27,12 @@ import (
|
||||
"github.com/grafana/nanogit/protocol/hash"
|
||||
)
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const gitTokenSecretSuffix = "-git-token"
|
||||
|
||||
type RepositoryConfig struct {
|
||||
URL string
|
||||
Branch string
|
||||
TokenUser string
|
||||
Token string
|
||||
EncryptedToken []byte
|
||||
Path string
|
||||
URL string
|
||||
Branch string
|
||||
TokenUser string
|
||||
Token common.RawSecureValue
|
||||
Path string
|
||||
}
|
||||
|
||||
// Make sure all public functions of this struct call the (*gitRepository).logger function, to ensure the Git repo details are included.
|
||||
@@ -44,23 +40,21 @@ type gitRepository struct {
|
||||
config *provisioning.Repository
|
||||
gitConfig RepositoryConfig
|
||||
client nanogit.Client
|
||||
secrets secrets.RepositorySecrets
|
||||
}
|
||||
|
||||
func NewGitRepository(
|
||||
ctx context.Context,
|
||||
config *provisioning.Repository,
|
||||
gitConfig RepositoryConfig,
|
||||
secrets secrets.RepositorySecrets,
|
||||
) (GitRepository, error) {
|
||||
var opts []options.Option
|
||||
if len(gitConfig.Token) > 0 {
|
||||
if !gitConfig.Token.IsZero() {
|
||||
tokenUser := gitConfig.TokenUser
|
||||
if tokenUser == "" {
|
||||
tokenUser = "git"
|
||||
}
|
||||
|
||||
opts = append(opts, options.WithBasicAuth(tokenUser, gitConfig.Token))
|
||||
opts = append(opts, options.WithBasicAuth(tokenUser, string(gitConfig.Token)))
|
||||
}
|
||||
|
||||
client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...)
|
||||
@@ -72,7 +66,6 @@ func NewGitRepository(
|
||||
config: config,
|
||||
gitConfig: gitConfig,
|
||||
client: client,
|
||||
secrets: secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -106,10 +99,10 @@ func (r *gitRepository) Validate() (list field.ErrorList) {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name"))
|
||||
}
|
||||
|
||||
// If the repository has workflows, we require a token or encrypted token
|
||||
// Readonly repositories may not need a token (if public)
|
||||
if len(r.config.Spec.Workflows) > 0 {
|
||||
if cfg.Token == "" && len(cfg.EncryptedToken) == 0 {
|
||||
list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required"))
|
||||
if cfg.Token == "" && r.config.Secure.Token.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "token"), "a git access token is required"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +159,7 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
|
||||
Success: false,
|
||||
Errors: []provisioning.ErrorDetails{{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", t, "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: detail,
|
||||
}},
|
||||
}, nil
|
||||
@@ -832,23 +825,3 @@ func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context
|
||||
|
||||
return ctx, logger
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnDelete(ctx context.Context) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
secretName := r.config.Name + gitTokenSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete git token secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted git token secret", "secretName", secretName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/nanogit"
|
||||
"github.com/grafana/nanogit/mocks"
|
||||
"github.com/grafana/nanogit/protocol"
|
||||
@@ -152,7 +151,7 @@ func TestGitRepository_Validate(t *testing.T) {
|
||||
Token: "", // Empty token
|
||||
},
|
||||
want: field.ErrorList{
|
||||
field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"),
|
||||
field.Required(field.NewPath("secure", "token"), "a git access token is required"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -291,10 +290,9 @@ func TestNewGit(t *testing.T) {
|
||||
Path: "configs",
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
// This should succeed in creating the client but won't be able to connect
|
||||
// We just test that the basic structure is created correctly
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gitRepo)
|
||||
require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL())
|
||||
@@ -502,7 +500,7 @@ func TestGitRepository_Test(t *testing.T) {
|
||||
Errors: []provisioning.ErrorDetails{
|
||||
{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", "test_type", "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: "failed check if authorized: auth error",
|
||||
},
|
||||
},
|
||||
@@ -523,7 +521,7 @@ func TestGitRepository_Test(t *testing.T) {
|
||||
Errors: []provisioning.ErrorDetails{
|
||||
{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", "test_type", "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: "not authorized",
|
||||
},
|
||||
},
|
||||
@@ -1862,8 +1860,7 @@ func TestNewGitRepository(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig)
|
||||
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
@@ -2822,8 +2819,7 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) {
|
||||
Path: "configs",
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
|
||||
|
||||
// We expect this to fail during client creation
|
||||
require.Error(t, err)
|
||||
@@ -3737,82 +3733,6 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(*secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful secret deletion",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
).Return(errors.New("failed to delete secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
expectedError: "delete git token secret: failed to delete secret",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockSecrets)
|
||||
|
||||
gitRepo := &gitRepository{
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
err := gitRepo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitRepository_Move(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// Factory creates new GitHub clients.
|
||||
@@ -20,16 +22,15 @@ func ProvideFactory() *Factory {
|
||||
return &Factory{}
|
||||
}
|
||||
|
||||
func (r *Factory) New(ctx context.Context, ghToken string) Client {
|
||||
func (r *Factory) New(ctx context.Context, ghToken common.RawSecureValue) Client {
|
||||
if r.Client != nil {
|
||||
return NewClient(github.NewClient(r.Client))
|
||||
}
|
||||
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: ghToken},
|
||||
)
|
||||
|
||||
if len(ghToken) > 0 {
|
||||
if !ghToken.IsZero() {
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: string(ghToken)},
|
||||
)
|
||||
tokenClient := oauth2.NewClient(ctx, tokenSrc)
|
||||
return NewClient(github.NewClient(tokenClient))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
@@ -503,168 +503,6 @@ func (_c *MockGithubRepository_Move_Call) RunAndReturn(run func(context.Context,
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnCreate provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnCreate")
|
||||
}
|
||||
|
||||
var r0 []map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreate'
|
||||
type MockGithubRepository_OnCreate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnCreate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnCreate(ctx interface{}) *MockGithubRepository_OnCreate_Call {
|
||||
return &MockGithubRepository_OnCreate_Call{Call: _e.mock.On("OnCreate", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnDelete provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnDelete(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnDelete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete'
|
||||
type MockGithubRepository_OnDelete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnDelete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnDelete(ctx interface{}) *MockGithubRepository_OnDelete_Call {
|
||||
return &MockGithubRepository_OnDelete_Call{Call: _e.mock.On("OnDelete", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) Return(_a0 error) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) RunAndReturn(run func(context.Context) error) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnUpdate provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnUpdate")
|
||||
}
|
||||
|
||||
var r0 []map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdate'
|
||||
type MockGithubRepository_OnUpdate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnUpdate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnUpdate(ctx interface{}) *MockGithubRepository_OnUpdate_Call {
|
||||
return &MockGithubRepository_OnUpdate_Call{Call: _e.mock.On("OnUpdate", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Owner provides a mock function with no fields
|
||||
func (_m *MockGithubRepository) Owner() string {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
func Mutator() controller.Mutator {
|
||||
return func(ctx context.Context, obj runtime.Object) error {
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
@@ -31,16 +30,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
repo.Spec.GitHub.URL = url
|
||||
}
|
||||
|
||||
if repo.Spec.GitHub.Token != "" {
|
||||
secretName := repo.Name + githubTokenSecretSuffix
|
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.GitHub.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.Spec.GitHub.EncryptedToken = nameOrValue
|
||||
repo.Spec.GitHub.Token = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,21 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
func TestMutator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
setupMocks func(*secrets.MockRepositorySecrets)
|
||||
expectedToken string
|
||||
expectedEncryptedToken string
|
||||
expectedError string
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "trims trailing .git and slash from GitHub URL",
|
||||
@@ -35,9 +31,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "trims only trailing slash from GitHub URL",
|
||||
@@ -52,9 +45,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "trims only trailing .git from GitHub URL",
|
||||
@@ -69,9 +59,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "does not trim if no .git or slash",
|
||||
@@ -86,76 +73,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "successful token encryption",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
"secret-token",
|
||||
).Return([]byte("encrypted-token"), nil)
|
||||
},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "encrypted-token",
|
||||
},
|
||||
{
|
||||
name: "encryption error",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
"secret-token",
|
||||
).Return(nil, errors.New("encryption failed"))
|
||||
},
|
||||
expectedError: "encryption failed",
|
||||
},
|
||||
{
|
||||
name: "no github spec",
|
||||
@@ -168,9 +85,6 @@ func TestMutator(t *testing.T) {
|
||||
GitHub: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
@@ -180,30 +94,19 @@ func TestMutator(t *testing.T) {
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "",
|
||||
},
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{},
|
||||
},
|
||||
},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-repository object",
|
||||
obj: &runtime.Unknown{},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMocks(mockSecrets)
|
||||
|
||||
mutator := Mutator(mockSecrets)
|
||||
mutator := Mutator()
|
||||
err := mutator(context.Background(), tt.obj)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
@@ -211,16 +114,6 @@ func TestMutator(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that token was cleared and encrypted token was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.GitHub != nil {
|
||||
if tt.expectedEncryptedToken != "" {
|
||||
// Token should be cleared after encryption
|
||||
assert.Empty(t, repo.Spec.GitHub.Token, "Token should be cleared after encryption")
|
||||
// EncryptedToken should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.GitHub.EncryptedToken), "EncryptedToken should match expected value")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,25 +7,20 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const githubTokenSecretSuffix = "-github-token"
|
||||
|
||||
// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included.
|
||||
type githubRepository struct {
|
||||
git.GitRepository
|
||||
config *provisioning.Repository
|
||||
gh Client // assumes github.com base URL
|
||||
secrets secrets.RepositorySecrets
|
||||
config *provisioning.Repository
|
||||
gh Client // assumes github.com base URL
|
||||
|
||||
owner string
|
||||
repo string
|
||||
@@ -42,7 +37,6 @@ type GithubRepository interface {
|
||||
repository.Reader
|
||||
repository.RepositoryWithURLs
|
||||
repository.StageableRepository
|
||||
repository.Hooks
|
||||
Owner() string
|
||||
Repo() string
|
||||
Client() Client
|
||||
@@ -53,8 +47,7 @@ func NewGitHub(
|
||||
config *provisioning.Repository,
|
||||
gitRepo git.GitRepository,
|
||||
factory *Factory,
|
||||
token string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
token common.RawSecureValue,
|
||||
) (GithubRepository, error) {
|
||||
owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
|
||||
if err != nil {
|
||||
@@ -67,7 +60,6 @@ func NewGitHub(
|
||||
gh: factory.New(ctx, token), // TODO, baseURL from config
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
secrets: secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -242,23 +234,3 @@ func (r *githubRepository) RefURLs(ctx context.Context, ref string) (*provisioni
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnDelete(ctx context.Context) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
secretName := r.config.Name + githubTokenSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete github token secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted github token secret", "secretName", secretName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
field "k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
func TestNewGitHub(t *testing.T) {
|
||||
@@ -81,16 +81,13 @@ func TestNewGitHub(t *testing.T) {
|
||||
|
||||
gitRepo := git.NewMockGitRepository(t)
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
|
||||
// Call the function under test
|
||||
repo, err := NewGitHub(
|
||||
context.Background(),
|
||||
tt.config,
|
||||
gitRepo,
|
||||
factory,
|
||||
tt.token,
|
||||
mockSecrets,
|
||||
common.RawSecureValue(tt.token),
|
||||
)
|
||||
|
||||
// Check results
|
||||
@@ -179,10 +176,14 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
Path: "dashboards",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetup: func(m *git.MockGitRepository) {
|
||||
m.On("Config").Return(&provisioning.Repository{
|
||||
@@ -190,10 +191,14 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
Path: "dashboards",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
m.On("Validate").Return(field.ErrorList{})
|
||||
},
|
||||
@@ -223,7 +228,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -233,7 +242,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -248,7 +261,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -258,7 +275,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -273,7 +294,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://gitlab.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -283,7 +308,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://gitlab.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -339,7 +368,11 @@ func TestGitHubRepositoryTest(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -361,7 +394,11 @@ func TestGitHubRepositoryTest(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -878,7 +915,11 @@ func TestGitHubRepositoryDelegation(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "test-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1095,7 +1136,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "test-token",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1137,82 +1177,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(*secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful secret deletion",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
).Return(errors.New("failed to delete secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
expectedError: "delete github token secret: failed to delete secret",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockSecrets)
|
||||
|
||||
githubRepo := &githubRepository{
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
err := githubRepo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGithubRepository_Move(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1262,7 +1226,6 @@ func TestGithubRepository_Move(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create mock git repository
|
||||
mockGitRepo := git.NewMockGitRepository(t)
|
||||
mockSecrets := &secrets.MockRepositorySecrets{}
|
||||
|
||||
// Setup mock expectations
|
||||
tt.setupMock(mockGitRepo)
|
||||
@@ -1285,7 +1248,6 @@ func TestGithubRepository_Move(t *testing.T) {
|
||||
GitRepository: mockGitRepo,
|
||||
owner: "example",
|
||||
repo: "repo",
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
// Execute move operation
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
type Decrypter = func(r *provisioning.Repository) SecureValues
|
||||
|
||||
type SecureValues interface {
|
||||
Token(ctx context.Context) (common.RawSecureValue, error)
|
||||
WebhookSecret(ctx context.Context) (common.RawSecureValue, error)
|
||||
}
|
||||
|
||||
type secureValues struct {
|
||||
svc contracts.DecryptService
|
||||
names provisioning.SecureValues
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (s *secureValues) get(ctx context.Context, sv common.InlineSecureValue) (common.RawSecureValue, error) {
|
||||
if !sv.Create.IsZero() {
|
||||
return sv.Create, nil // If this was called before the value is actually saved
|
||||
}
|
||||
if sv.Name == "" {
|
||||
return "", nil
|
||||
}
|
||||
results, err := s.svc.Decrypt(ctx, provisioning.GROUP, s.namespace, sv.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to call decrypt service: %w", err)
|
||||
}
|
||||
|
||||
v, found := results[sv.Name]
|
||||
if !found {
|
||||
return "", fmt.Errorf("not found")
|
||||
}
|
||||
if v.Error() != nil {
|
||||
return "", v.Error()
|
||||
}
|
||||
return common.RawSecureValue(*v.Value()), nil
|
||||
}
|
||||
|
||||
func (s *secureValues) Token(ctx context.Context) (common.RawSecureValue, error) {
|
||||
return s.get(ctx, s.names.Token)
|
||||
}
|
||||
|
||||
func (s *secureValues) WebhookSecret(ctx context.Context) (common.RawSecureValue, error) {
|
||||
return s.get(ctx, s.names.WebhookSecret)
|
||||
}
|
||||
|
||||
func DecryptService(svc contracts.DecryptService) Decrypter {
|
||||
return func(r *provisioning.Repository) SecureValues {
|
||||
return &secureValues{svc: svc, names: r.Secure, namespace: r.Namespace}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestRepositorySecureValues(t *testing.T) {
|
||||
type expectedDecryptedResult struct {
|
||||
value string
|
||||
error string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *provisioning.Repository
|
||||
decrypt decryptFn
|
||||
token expectedDecryptedResult
|
||||
webhook expectedDecryptedResult
|
||||
}{
|
||||
{
|
||||
name: "referenced by name",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
require.Equal(t, []string{"secret"}, names)
|
||||
val := secretv1beta1.NewExposedSecureValue(names[0])
|
||||
return map[string]contracts.DecryptResult{
|
||||
names[0]: contracts.NewDecryptResultValue(&val),
|
||||
}, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
value: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when create exists, use it",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Create: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
t.Fatal("decrypt should not be called when Create is set")
|
||||
return nil, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
value: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "avoid decrypt when no values are configured",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
t.Fatal("decrypt should not be called when no values are configured")
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "propagate error from service",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
require.Equal(t, []string{"secret"}, names)
|
||||
return map[string]contracts.DecryptResult{
|
||||
names[0]: contracts.NewDecryptResultErr(fmt.Errorf("error for name")),
|
||||
}, nil
|
||||
},
|
||||
webhook: expectedDecryptedResult{
|
||||
error: "error for name",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return map[string]contracts.DecryptResult{}, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
error: "not found", // it is not in the results above
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return nil, fmt.Errorf("explode")
|
||||
},
|
||||
webhook: expectedDecryptedResult{
|
||||
error: "failed to call decrypt service",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decrypter := DecryptService(&dummyDecryptService{t: t, fn: tt.decrypt})
|
||||
decrypted := decrypter(tt.config)
|
||||
|
||||
token, err := decrypted.Token(context.Background())
|
||||
if tt.token.error != "" {
|
||||
require.ErrorContains(t, err, tt.token.error)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.token.value, string(token))
|
||||
}
|
||||
|
||||
webhook, err := decrypted.WebhookSecret(context.Background())
|
||||
if tt.webhook.error != "" {
|
||||
require.ErrorContains(t, err, tt.webhook.error)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.webhook.value, string(webhook))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type decryptFn = func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error)
|
||||
|
||||
type dummyDecryptService struct {
|
||||
t *testing.T
|
||||
fn decryptFn
|
||||
}
|
||||
|
||||
func (d *dummyDecryptService) Decrypt(_ context.Context, _ string, _ string, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return d.fn(d.t, names...)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
)
|
||||
|
||||
// A secrets encryption service. It only operates on values, no names or similar.
|
||||
// It is likely we will need to change this when the multi-tenant service comes around.
|
||||
//
|
||||
// FIXME: this is a temporary service/package until we can make use of
|
||||
// the new secrets service in app platform.
|
||||
//
|
||||
//go:generate mockery --name LegacyService --structname MockLegacyService --inpackage --filename legacy_secret_mock.go --with-expecter
|
||||
type LegacyService interface {
|
||||
Encrypt(ctx context.Context, data []byte) ([]byte, error)
|
||||
Decrypt(ctx context.Context, data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
var _ LegacyService = (*singleTenant)(nil)
|
||||
|
||||
type singleTenant struct {
|
||||
inner secrets.Service
|
||||
}
|
||||
|
||||
func NewSingleTenant(svc secrets.Service) LegacyService {
|
||||
return &singleTenant{svc}
|
||||
}
|
||||
|
||||
func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
return s.inner.Encrypt(ctx, data, secrets.WithoutScope())
|
||||
}
|
||||
|
||||
func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
return s.inner.Decrypt(ctx, data)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockLegacyService is an autogenerated mock type for the LegacyService type
|
||||
type MockLegacyService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockLegacyService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockLegacyService) EXPECT() *MockLegacyService_Expecter {
|
||||
return &MockLegacyService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, data
|
||||
func (_m *MockLegacyService) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
ret := _m.Called(ctx, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok {
|
||||
return rf(ctx, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok {
|
||||
r0 = rf(ctx, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok {
|
||||
r1 = rf(ctx, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockLegacyService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockLegacyService_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - data []byte
|
||||
func (_e *MockLegacyService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockLegacyService_Decrypt_Call {
|
||||
return &MockLegacyService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)}
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, data
|
||||
func (_m *MockLegacyService) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
ret := _m.Called(ctx, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok {
|
||||
return rf(ctx, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok {
|
||||
r0 = rf(ctx, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok {
|
||||
r1 = rf(ctx, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockLegacyService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockLegacyService_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - data []byte
|
||||
func (_e *MockLegacyService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockLegacyService_Encrypt_Call {
|
||||
return &MockLegacyService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)}
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockLegacyService creates a new instance of MockLegacyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockLegacyService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockLegacyService {
|
||||
mock := &MockLegacyService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
legacysecrets "github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideRepositorySecrets(
|
||||
features featuremgmt.FeatureToggles,
|
||||
legacySecretsSvc legacysecrets.Service,
|
||||
secretsSvc contracts.SecureValueClient,
|
||||
decryptSvc secret.DecryptService,
|
||||
cfg *setting.Cfg,
|
||||
) RepositorySecrets {
|
||||
return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc, cfg.SecretsManagement.GrpcGrafanaServiceName), NewSingleTenant(legacySecretsSvc))
|
||||
}
|
||||
|
||||
//go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter
|
||||
type RepositorySecrets interface {
|
||||
Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error)
|
||||
Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) (data []byte, err error)
|
||||
Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error
|
||||
}
|
||||
|
||||
// repositorySecrets provides a unified interface for encrypting and decrypting repository secrets,
|
||||
// supporting both the legacy and new secrets services. The active backend is determined by the
|
||||
// FlagProvisioningSecretsService feature flag:
|
||||
// - If enabled, operations use the new secrets service.
|
||||
// - If disabled, operations use the legacy secrets service.
|
||||
//
|
||||
// This abstraction enables a seamless migration path between secret backends without breaking
|
||||
// existing functionality. Once migration is complete and the legacy service is deprecated,
|
||||
// this wrapper should be removed.
|
||||
type repositorySecrets struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
secretsSvc Service
|
||||
legacySecrets LegacyService
|
||||
}
|
||||
|
||||
func NewRepositorySecrets(features featuremgmt.FeatureToggles, secretsSvc Service, legacySecrets LegacyService) RepositorySecrets {
|
||||
return &repositorySecrets{
|
||||
features: features,
|
||||
secretsSvc: secretsSvc,
|
||||
legacySecrets: legacySecrets,
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts the data and returns the name or value of the encrypted data
|
||||
// If the feature flag is disabled, it uses the legacy secrets service
|
||||
// If the feature flag is enabled, it uses the secrets service
|
||||
func (s *repositorySecrets) Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) {
|
||||
logger := logging.FromContext(ctx).With("name", name, "namespace", r.GetNamespace())
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) {
|
||||
logger.Info("Encrypting secret with new secrets service")
|
||||
encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(encrypted), err
|
||||
}
|
||||
|
||||
logger.Info("Encrypting secret with legacy secrets service")
|
||||
encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// Decrypt retrieves and decrypts secret data for a repository, supporting migration between secret backends.
|
||||
// The backend used for decryption is determined by a heuristic:
|
||||
// - If the provided nameOrValue starts with the repository name, it is assumed to be a Kubernetes secret name
|
||||
// and the new secrets service is used for decryption.
|
||||
// - Otherwise, it is treated as a legacy secret value and the legacy secrets service is used.
|
||||
//
|
||||
// HACK: This approach relies on checking the prefix of nameOrValue to distinguish between secret backends.
|
||||
// This is a temporary workaround to support both backends during migration and should be removed once
|
||||
// migration is complete.
|
||||
//
|
||||
// This method ensures compatibility and minimizes disruption during the transition between secret backends.
|
||||
func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) {
|
||||
logger := logging.FromContext(ctx)
|
||||
// HACK: this is a hack to identify if the name is a potential Kubernetes name for a secret.
|
||||
if strings.HasPrefix(nameOrValue, r.GetName()) {
|
||||
logger.Info("Decrypting secret with new secrets service")
|
||||
return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue)
|
||||
} else {
|
||||
logger.Info("Decrypting secret with legacy secrets service")
|
||||
return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *repositorySecrets) Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error {
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) {
|
||||
err := s.secretsSvc.Delete(ctx, r.GetNamespace(), nameOrValue)
|
||||
if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockRepositorySecrets is an autogenerated mock type for the RepositorySecrets type
|
||||
type MockRepositorySecrets struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockRepositorySecrets_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockRepositorySecrets) EXPECT() *MockRepositorySecrets_Expecter {
|
||||
return &MockRepositorySecrets_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, r, nameOrValue
|
||||
func (_m *MockRepositorySecrets) Decrypt(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, r, nameOrValue)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) ([]byte, error)); ok {
|
||||
return rf(ctx, r, nameOrValue)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) []byte); ok {
|
||||
r0 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string) error); ok {
|
||||
r1 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockRepositorySecrets_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - nameOrValue string
|
||||
func (_e *MockRepositorySecrets_Expecter) Decrypt(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Decrypt_Call {
|
||||
return &MockRepositorySecrets_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, r, nameOrValue)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) Return(data []byte, err error) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Return(data, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) ([]byte, error)) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, r, nameOrValue
|
||||
func (_m *MockRepositorySecrets) Delete(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) error {
|
||||
ret := _m.Called(ctx, r, nameOrValue)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) error); ok {
|
||||
r0 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type MockRepositorySecrets_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - nameOrValue string
|
||||
func (_e *MockRepositorySecrets_Expecter) Delete(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Delete_Call {
|
||||
return &MockRepositorySecrets_Delete_Call{Call: _e.mock.On("Delete", ctx, r, nameOrValue)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) Return(_a0 error) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) error) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, r, name, data
|
||||
func (_m *MockRepositorySecrets) Encrypt(ctx context.Context, r *v0alpha1.Repository, name string, data string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, r, name, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)); ok {
|
||||
return rf(ctx, r, name, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) []byte); ok {
|
||||
r0 = rf(ctx, r, name, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string, string) error); ok {
|
||||
r1 = rf(ctx, r, name, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockRepositorySecrets_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - name string
|
||||
// - data string
|
||||
func (_e *MockRepositorySecrets_Expecter) Encrypt(ctx interface{}, r interface{}, name interface{}, data interface{}) *MockRepositorySecrets_Encrypt_Call {
|
||||
return &MockRepositorySecrets_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, r, name, data)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, name string, data string)) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) Return(nameOrValue []byte, err error) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Return(nameOrValue, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockRepositorySecrets creates a new instance of MockRepositorySecrets. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockRepositorySecrets(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockRepositorySecrets {
|
||||
mock := &MockRepositorySecrets{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type testSetup struct {
|
||||
rs RepositorySecrets
|
||||
mockFeatures *featuremgmt.MockFeatureToggles
|
||||
mockSecrets *MockService
|
||||
mockLegacy *MockLegacyService
|
||||
repo *provisioning.Repository
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T, namespace string) *testSetup {
|
||||
mockFeatures := featuremgmt.NewMockFeatureToggles(t)
|
||||
mockSecrets := NewMockService(t)
|
||||
mockLegacy := NewMockLegacyService(t)
|
||||
|
||||
rs := NewRepositorySecrets(mockFeatures, mockSecrets, mockLegacy)
|
||||
|
||||
repo := &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
return &testSetup{
|
||||
rs: rs,
|
||||
mockFeatures: mockFeatures,
|
||||
mockSecrets: mockSecrets,
|
||||
mockLegacy: mockLegacy,
|
||||
repo: repo,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testSetup) expectFeatureFlag(enabled bool) {
|
||||
s.mockFeatures.EXPECT().IsEnabled(
|
||||
mock.AnythingOfType("context.backgroundCtx"),
|
||||
featuremgmt.FlagProvisioningSecretsService,
|
||||
).Return(enabled)
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Encrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
featureEnabled bool
|
||||
setupMocks func(*testSetup)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("encrypted-name", nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted-name"),
|
||||
},
|
||||
{
|
||||
name: "legacy service success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: false,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return([]byte("encrypted-legacy-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted-legacy-data"),
|
||||
},
|
||||
{
|
||||
name: "new service error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("", errors.New("encryption failed"))
|
||||
},
|
||||
expectedError: "encryption failed",
|
||||
},
|
||||
{
|
||||
name: "legacy service error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: false,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return(nil, errors.New("legacy encryption failed"))
|
||||
},
|
||||
expectedError: "legacy encryption failed",
|
||||
},
|
||||
{
|
||||
name: "empty namespace handling",
|
||||
namespace: "",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "", "test-secret", "secret-data").Return("encrypted", nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
result, err := setup.rs.Encrypt(setup.ctx, setup.repo, "test-secret", "secret-data")
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Decrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
nameOrValue string
|
||||
setupMocks func(*testSetup)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service success - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte("decrypted-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-data"),
|
||||
},
|
||||
{
|
||||
name: "new service error - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return(nil, errors.New("new service failed"))
|
||||
},
|
||||
expectedError: "new service failed",
|
||||
},
|
||||
{
|
||||
name: "legacy service success - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte("decrypted-legacy-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-legacy-data"),
|
||||
},
|
||||
{
|
||||
name: "legacy service error - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return(nil, errors.New("legacy service failed"))
|
||||
},
|
||||
expectedError: "legacy service failed",
|
||||
},
|
||||
{
|
||||
name: "new service empty bytes - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte{}, nil)
|
||||
},
|
||||
expectedResult: []byte{},
|
||||
},
|
||||
{
|
||||
name: "legacy service empty bytes - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte{}, nil)
|
||||
},
|
||||
expectedResult: []byte{},
|
||||
},
|
||||
{
|
||||
name: "custom namespace handling - name starts with repo name",
|
||||
namespace: "custom-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "test-repo-secret-name").Return([]byte("test-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("test-data"),
|
||||
},
|
||||
{
|
||||
name: "exact repo name match - should use new service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo").Return([]byte("exact-match-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("exact-match-data"),
|
||||
},
|
||||
{
|
||||
name: "partial repo name match - should use legacy service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-rep",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("test-rep")).Return([]byte("partial-match-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("partial-match-data"),
|
||||
},
|
||||
{
|
||||
name: "empty name - should use legacy service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("")).Return([]byte("empty-name-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("empty-name-data"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
result, err := setup.rs.Decrypt(setup.ctx, setup.repo, tt.nameOrValue)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
featureEnabled bool
|
||||
setupMocks func(*testSetup)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service delete success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new service delete error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(errors.New("delete failed"))
|
||||
},
|
||||
expectedError: "delete failed",
|
||||
},
|
||||
{
|
||||
name: "new service secret not found - should succeed",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "non-existent-secret").Return(contracts.ErrSecureValueNotFound)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nothing for legacy",
|
||||
namespace: "custom-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
secretName := "secret-to-delete"
|
||||
if tt.name == "new service secret not found - should succeed" {
|
||||
secretName = "non-existent-secret"
|
||||
}
|
||||
err := setup.rs.Delete(setup.ctx, setup.repo, secretName)
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
setup.mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
const svcName = provisioning.GROUP
|
||||
|
||||
//go:generate mockery --name SecureValueClient --structname MockSecureValueClient --inpackage --filename secure_value_client_mock.go --with-expecter
|
||||
type SecureValueClient = secret.SecureValueClient
|
||||
|
||||
//go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter
|
||||
type Service interface {
|
||||
Encrypt(ctx context.Context, namespace, name string, data string) (string, error)
|
||||
Decrypt(ctx context.Context, namespace string, name string) ([]byte, error)
|
||||
Delete(ctx context.Context, namespace string, name string) error
|
||||
}
|
||||
|
||||
var _ Service = (*secretsService)(nil)
|
||||
|
||||
//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret --filename decrypt_service_mock.go --with-expecter
|
||||
type secretsService struct {
|
||||
secureValues SecureValueClient
|
||||
decryptSvc secret.DecryptService
|
||||
decrypterServiceName string
|
||||
}
|
||||
|
||||
func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService, grpcGrafanaServiceName string) Service {
|
||||
return &secretsService{
|
||||
secureValues: secretsSvc,
|
||||
decryptSvc: decryptSvc,
|
||||
decrypterServiceName: grpcGrafanaServiceName,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, data string) (string, error) {
|
||||
client, err := s.secureValues.Client(ctx, namespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try to get existing secret
|
||||
existingUnstructured, err := client.Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// If secret doesn't exist (not found error), we'll create it
|
||||
// For other errors, return the error
|
||||
if !errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
// Check if it's a k8s not found error
|
||||
if !isNotFoundError(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingUnstructured != nil {
|
||||
// Update the value directly in the unstructured object
|
||||
if err := unstructured.SetNestedField(existingUnstructured.Object, data, "spec", "value"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update using dynamic client
|
||||
result, err := client.Update(ctx, existingUnstructured, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.GetName(), nil
|
||||
}
|
||||
|
||||
decrypters := []string{svcName}
|
||||
if s.decrypterServiceName != "" {
|
||||
decrypters = append(decrypters, s.decrypterServiceName)
|
||||
}
|
||||
|
||||
// Create the secret directly as unstructured
|
||||
secret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "secret.grafana.app/v1beta1",
|
||||
"kind": "SecureValue",
|
||||
"metadata": map[string]interface{}{
|
||||
"namespace": namespace,
|
||||
"name": name,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: " + name,
|
||||
"value": data,
|
||||
"decrypters": decrypters,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create new secret
|
||||
finalSecret, err := client.Create(ctx, secret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return finalSecret.GetName(), nil
|
||||
}
|
||||
|
||||
func (s *secretsService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) {
|
||||
results, err := s.decryptSvc.Decrypt(ctx, svcName, namespace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res, ok := results[name]; ok {
|
||||
if res.Error() == nil {
|
||||
return []byte(res.Value().DangerouslyExposeAndConsumeValue()), nil
|
||||
}
|
||||
|
||||
return nil, res.Error()
|
||||
}
|
||||
|
||||
return nil, contracts.ErrDecryptNotFound
|
||||
}
|
||||
|
||||
func (s *secretsService) Delete(ctx context.Context, namespace string, name string) error {
|
||||
client, err := s.secureValues.Client(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
|
||||
// FIXME: This is a temporary workaround until the client abstraction properly handles
|
||||
// k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound
|
||||
if isNotFoundError(err) {
|
||||
return contracts.ErrSecureValueNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to check if error is a not found error
|
||||
// FIXME: This is a temporary workaround until the client abstraction properly handles
|
||||
// k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound
|
||||
func isNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for Grafana's secure value not found error
|
||||
if errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for k8s not found error
|
||||
if apierrors.IsNotFound(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback for generic not found error messages
|
||||
return err.Error() == "not found"
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockService is an autogenerated mock type for the Service type
|
||||
type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockService) EXPECT() *MockService_Expecter {
|
||||
return &MockService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, namespace, name
|
||||
func (_m *MockService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, namespace, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok {
|
||||
return rf(ctx, namespace, name)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok {
|
||||
r0 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockService_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
func (_e *MockService_Expecter) Decrypt(ctx interface{}, namespace interface{}, name interface{}) *MockService_Decrypt_Call {
|
||||
return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, namespace, name)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockService_Decrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockService_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, namespace, name
|
||||
func (_m *MockService) Delete(ctx context.Context, namespace string, name string) error {
|
||||
ret := _m.Called(ctx, namespace, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type MockService_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
func (_e *MockService_Expecter) Delete(ctx interface{}, namespace interface{}, name interface{}) *MockService_Delete_Call {
|
||||
return &MockService_Delete_Call{Call: _e.mock.On("Delete", ctx, namespace, name)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) Return(_a0 error) *MockService_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) RunAndReturn(run func(context.Context, string, string) error) *MockService_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, namespace, name, data
|
||||
func (_m *MockService) Encrypt(ctx context.Context, namespace string, name string, data string) (string, error) {
|
||||
ret := _m.Called(ctx, namespace, name, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok {
|
||||
return rf(ctx, namespace, name, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok {
|
||||
r0 = rf(ctx, namespace, name, data)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
|
||||
r1 = rf(ctx, namespace, name, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockService_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
// - data string
|
||||
func (_e *MockService_Expecter) Encrypt(ctx interface{}, namespace interface{}, name interface{}, data interface{}) *MockService_Encrypt_Call {
|
||||
return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, namespace, name, data)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, namespace string, name string, data string)) *MockService_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) Return(_a0 string, _a1 error) *MockService_Encrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, string, string, string) (string, error)) *MockService_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockService {
|
||||
mock := &MockService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface
|
||||
type mockDynamicInterface struct {
|
||||
dynamic.ResourceInterface
|
||||
getResult *unstructured.Unstructured
|
||||
getErr error
|
||||
createResult *unstructured.Unstructured
|
||||
createErr error
|
||||
updateResult *unstructured.Unstructured
|
||||
updateErr error
|
||||
deleteErr error
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.getResult, m.getErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.createResult, m.createErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.updateResult, m.updateErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
return m.deleteErr
|
||||
}
|
||||
|
||||
func TestNewSecretsService(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
assert.NotNil(t, svc)
|
||||
assert.IsType(t, &secretsService{}, svc)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test is complex but it's a good test for the SecretsService.
|
||||
func TestSecretsService_Encrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
data string
|
||||
setupMocks func(*MockSecureValueClient, *secret.MockDecryptService, *mockDynamicInterface)
|
||||
expectedName string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successfully create new secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return not found error (secret doesn't exist)
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
// Mock Create call
|
||||
mockResourceInterface.createResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.createErr = nil
|
||||
},
|
||||
expectedName: "test-secret",
|
||||
},
|
||||
{
|
||||
name: "successfully update existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "existing-secret",
|
||||
data: "new-secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
existingSecret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: existing-secret",
|
||||
"decrypters": []string{svcName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return existing secret
|
||||
mockResourceInterface.getResult = existingSecret
|
||||
mockResourceInterface.getErr = nil
|
||||
|
||||
// Mock Update call
|
||||
mockResourceInterface.updateResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.updateErr = nil
|
||||
},
|
||||
expectedName: "existing-secret",
|
||||
},
|
||||
{
|
||||
name: "error reading existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return error
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = errors.New("database error")
|
||||
},
|
||||
expectedError: "database error",
|
||||
},
|
||||
{
|
||||
name: "error creating new secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return not found error
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
// Mock Create call to return error
|
||||
mockResourceInterface.createResult = nil
|
||||
mockResourceInterface.createErr = errors.New("creation failed")
|
||||
},
|
||||
expectedError: "creation failed",
|
||||
},
|
||||
{
|
||||
name: "error updating existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "existing-secret",
|
||||
data: "new-secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
existingSecret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: existing-secret",
|
||||
"decrypters": []string{svcName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return existing secret
|
||||
mockResourceInterface.getResult = existingSecret
|
||||
mockResourceInterface.getErr = nil
|
||||
|
||||
// Mock Update call to return error
|
||||
mockResourceInterface.updateResult = nil
|
||||
mockResourceInterface.updateErr = errors.New("update failed")
|
||||
},
|
||||
expectedError: "update failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, tt.namespace, tt.secretName, tt.data)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedName, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsService_Encrypt_ClientError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
// Setup client to return error
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error"))
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "client error")
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Decrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
setupMocks func(*MockSecureValueClient, *secret.MockDecryptService)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successfully decrypt secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data")
|
||||
mockResult := secret.NewDecryptResultValue(&exposedValue)
|
||||
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
// Verify that the context is not nil (the service creates a new StaticRequester)
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-data"),
|
||||
},
|
||||
{
|
||||
name: "decrypt service error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(nil, errors.New("decrypt service error"))
|
||||
},
|
||||
expectedError: "decrypt service error",
|
||||
},
|
||||
{
|
||||
name: "secret not found in results",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{}, nil)
|
||||
},
|
||||
expectedError: secret.ErrDecryptNotFound.Error(),
|
||||
},
|
||||
{
|
||||
name: "decrypt result has error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockResult := secret.NewDecryptResultErr(errors.New("decryption failed"))
|
||||
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
},
|
||||
expectedError: "decryption failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Decrypt(ctx, tt.namespace, tt.secretName)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test to verify that the Decrypt method creates the correct service identity context
|
||||
func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
exposedValue := secretv1beta1.NewExposedSecureValue("test-data")
|
||||
mockResult := secret.NewDecryptResultValue(&exposedValue)
|
||||
|
||||
// Create a more detailed context matcher to verify the service identity context is created correctly
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
// At minimum, verify the context is not nil and is different from the original
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := svc.Decrypt(ctx, "test-namespace", "test-secret")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("test-data"), result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
setupMocks func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "delete success",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call
|
||||
mockResourceInterface.deleteErr = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete returns error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return error
|
||||
mockResourceInterface.deleteErr = errors.New("delete failed")
|
||||
},
|
||||
expectedError: "delete failed",
|
||||
},
|
||||
{
|
||||
name: "client error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return error
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error"))
|
||||
},
|
||||
expectedError: "client error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, tt.namespace, tt.secretName)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotFoundError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "grafana secure value not found error",
|
||||
err: contracts.ErrSecureValueNotFound,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "k8s not found error",
|
||||
err: apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "generic not found error message",
|
||||
err: errors.New("not found"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("internal server error"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped grafana error",
|
||||
err: errors.New("wrapped: " + contracts.ErrSecureValueNotFound.Error()),
|
||||
expected: false, // wrapped errors won't match errors.Is unless properly wrapped
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isNotFoundError(tt.err)
|
||||
assert.Equal(t, tt.expected, result, "isNotFoundError(%v) = %v, want %v", tt.err, result, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsService_Encrypt_WithK8sNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return k8s not found error
|
||||
k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret")
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = k8sNotFoundErr
|
||||
|
||||
// Mock Create call to succeed
|
||||
mockResourceInterface.createResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.createErr = nil
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-secret", result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete_WithK8sNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return k8s not found error
|
||||
k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret")
|
||||
mockResourceInterface.deleteErr = k8sNotFoundErr
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, "test-namespace", "test-secret")
|
||||
|
||||
// Should return contracts.ErrSecureValueNotFound instead of k8s error
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete_WithGrafanaNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return Grafana not found error
|
||||
mockResourceInterface.deleteErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, "test-namespace", "test-secret")
|
||||
|
||||
// Should return contracts.ErrSecureValueNotFound
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
dynamic "k8s.io/client-go/dynamic"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockSecureValueClient is an autogenerated mock type for the SecureValueClient type
|
||||
type MockSecureValueClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSecureValueClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSecureValueClient) EXPECT() *MockSecureValueClient_Expecter {
|
||||
return &MockSecureValueClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Client provides a mock function with given fields: ctx, namespace
|
||||
func (_m *MockSecureValueClient) Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) {
|
||||
ret := _m.Called(ctx, namespace)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Client")
|
||||
}
|
||||
|
||||
var r0 dynamic.ResourceInterface
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (dynamic.ResourceInterface, error)); ok {
|
||||
return rf(ctx, namespace)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) dynamic.ResourceInterface); ok {
|
||||
r0 = rf(ctx, namespace)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(dynamic.ResourceInterface)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, namespace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSecureValueClient_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client'
|
||||
type MockSecureValueClient_Client_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Client is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
func (_e *MockSecureValueClient_Expecter) Client(ctx interface{}, namespace interface{}) *MockSecureValueClient_Client_Call {
|
||||
return &MockSecureValueClient_Client_Call{Call: _e.mock.On("Client", ctx, namespace)}
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) Run(run func(ctx context.Context, namespace string)) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) Return(_a0 dynamic.ResourceInterface, _a1 error) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) RunAndReturn(run func(context.Context, string) (dynamic.ResourceInterface, error)) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSecureValueClient creates a new instance of MockSecureValueClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockSecureValueClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSecureValueClient {
|
||||
mock := &MockSecureValueClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -89,22 +89,31 @@ func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.O
|
||||
// HACK: Set the name and namespace if not set so that the temporary repository can be created
|
||||
// This can be removed once we deprecate legacy secrets is deprecated or we use InLineSecureValues as we
|
||||
// use the same field and repository name to detect which one to use.
|
||||
if cfg.GetName() == "" {
|
||||
if name == "new" {
|
||||
// HACK: frontend is passing a "new" we need to remove the hack there as well
|
||||
// Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test"
|
||||
name = "hack-on-hack-for-new"
|
||||
if name == "new" {
|
||||
// HACK: frontend is passing a "new" we need to remove the hack there as well
|
||||
// Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test"
|
||||
name = "hack-on-hack-for-new"
|
||||
} else {
|
||||
// Copy previous secure values if they exist
|
||||
old, _ := s.getter.GetRepository(ctx, name)
|
||||
if old != nil && !old.Config().Secure.IsZero() {
|
||||
secure := old.Config().Secure
|
||||
if cfg.Secure.Token.IsZero() {
|
||||
cfg.Secure.Token = secure.Token
|
||||
}
|
||||
if cfg.Secure.WebhookSecret.IsZero() {
|
||||
cfg.Secure.WebhookSecret = secure.WebhookSecret
|
||||
}
|
||||
}
|
||||
|
||||
cfg.SetName(name)
|
||||
}
|
||||
|
||||
cfg.SetName(name)
|
||||
if cfg.GetNamespace() == "" {
|
||||
cfg.SetNamespace(ns)
|
||||
}
|
||||
|
||||
// Create a temporary repository
|
||||
tmp, err := s.getter.AsRepository(ctx, &cfg)
|
||||
tmp, err := s.getter.RepositoryFromConfig(ctx, &cfg)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
|
||||
@@ -18,7 +18,7 @@ type RepoGetter interface {
|
||||
// Given a repository configuration, return it as a repository instance
|
||||
// This will only error for un-recoverable system errors
|
||||
// the repository instance may or may not be valid/healthy
|
||||
AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
RepositoryFromConfig(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
}
|
||||
|
||||
type ClientGetter interface {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
return func(ctx context.Context, obj runtime.Object) error {
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if repo.Status.Webhook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if repo.Status.Webhook.Secret != "" {
|
||||
secretName := repo.Name + webhookSecretSuffix
|
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Status.Webhook.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.Status.Webhook.EncryptedSecret = nameOrValue
|
||||
repo.Status.Webhook.Secret = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestMutator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj runtime.Object
|
||||
secret string
|
||||
setupMocks func(*secrets.MockRepositorySecrets)
|
||||
expectedEncryptedSecret string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful secret encryption",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
Secret: "webhook-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
Secret: "webhook-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo-webhook-secret",
|
||||
"webhook-secret",
|
||||
).Return([]byte("encrypted-webhook-secret"), nil)
|
||||
},
|
||||
expectedEncryptedSecret: "encrypted-webhook-secret",
|
||||
},
|
||||
{
|
||||
name: "encryption error",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
Secret: "webhook-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Encrypt(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
Secret: "webhook-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo-webhook-secret",
|
||||
"webhook-secret",
|
||||
).Return(nil, errors.New("encryption failed"))
|
||||
},
|
||||
expectedError: "encryption failed",
|
||||
},
|
||||
{
|
||||
name: "no webhook status",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty secret",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
Secret: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-repository object",
|
||||
obj: &runtime.Unknown{},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMocks(mockSecrets)
|
||||
|
||||
mutator := Mutator(mockSecrets)
|
||||
err := mutator(context.Background(), tt.obj)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that secret was cleared and encrypted secret was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Status.Webhook != nil {
|
||||
if tt.expectedEncryptedSecret != "" {
|
||||
// Secret should be cleared after encryption
|
||||
assert.Empty(t, repo.Status.Webhook.Secret, "Secret should be cleared after encryption")
|
||||
// EncryptedSecret should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedSecret, string(repo.Status.Webhook.EncryptedSecret), "EncryptedSecret should match expected value")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -47,7 +46,6 @@ func isPublicURL(url string) bool {
|
||||
func ProvideWebhooks(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
ghFactory *github.Factory,
|
||||
renderer rendering.Service,
|
||||
blobstore resource.ResourceClient,
|
||||
@@ -79,7 +77,6 @@ func ProvideWebhooks(
|
||||
render,
|
||||
webhook,
|
||||
urlProvider,
|
||||
repositorySecrets,
|
||||
ghFactory,
|
||||
parsers,
|
||||
[]jobs.Worker{pullRequestWorker},
|
||||
@@ -95,7 +92,6 @@ type WebhookExtra struct {
|
||||
render *renderConnector
|
||||
webhook *webhookConnector
|
||||
urlProvider func(namespace string) string
|
||||
secrets secrets.RepositorySecrets
|
||||
ghFactory *github.Factory
|
||||
parsers resources.ParserFactory
|
||||
workers []jobs.Worker
|
||||
@@ -106,7 +102,6 @@ func NewWebhookExtra(
|
||||
render *renderConnector,
|
||||
webhook *webhookConnector,
|
||||
urlProvider func(namespace string) string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
ghFactory *github.Factory,
|
||||
parsers resources.ParserFactory,
|
||||
workers []jobs.Worker,
|
||||
@@ -116,7 +111,6 @@ func NewWebhookExtra(
|
||||
render: render,
|
||||
webhook: webhook,
|
||||
urlProvider: urlProvider,
|
||||
secrets: secrets,
|
||||
ghFactory: ghFactory,
|
||||
parsers: parsers,
|
||||
workers: workers,
|
||||
@@ -136,9 +130,7 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) (
|
||||
|
||||
// Mutators returns the mutators for the webhook extra
|
||||
func (e *WebhookExtra) Mutators() []controller.Mutator {
|
||||
return []controller.Mutator{
|
||||
Mutator(e.secrets),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStorage updates the storage with both render and webhook connectors
|
||||
@@ -164,7 +156,7 @@ func (e *WebhookExtra) GetJobWorkers() []jobs.Worker {
|
||||
}
|
||||
|
||||
// AsRepository delegates repository creation to the webhook connector
|
||||
func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error) {
|
||||
// Only handle GitHub repositories with webhooks if URL is public
|
||||
if r.Spec.Type == provisioning.GitHubRepositoryType && e.isPublic {
|
||||
gvr := provisioning.RepositoryResourceInfo.GroupVersionResource()
|
||||
@@ -186,34 +178,33 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit
|
||||
}
|
||||
|
||||
// Decrypt GitHub token if needed
|
||||
ghToken := ghCfg.Token
|
||||
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
|
||||
decrypted, err := e.secrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
ghToken = string(decrypted)
|
||||
ghToken, err := secure.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
webhookSecret, err := secure.WebhookSecret(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt webhookSecret: %w", err)
|
||||
}
|
||||
|
||||
gitCfg := git.RepositoryConfig{
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
EncryptedToken: ghCfg.EncryptedToken,
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
}
|
||||
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, e.secrets)
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating git repository: %w", err)
|
||||
}
|
||||
|
||||
basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken, e.secrets)
|
||||
basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating github repository: %w", err)
|
||||
}
|
||||
|
||||
return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil
|
||||
return NewGithubWebhookRepository(basicRepo, webhookURL, webhookSecret), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -10,18 +10,16 @@ import (
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
"github.com/google/uuid"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
var subscribedEvents = []string{"push", "pull_request"}
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const webhookSecretSuffix = "-webhook-secret"
|
||||
var subscribedEvents = []string{"pull_request", "push"} // same order as slices.Sort()
|
||||
|
||||
type WebhookRepository interface {
|
||||
Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error)
|
||||
@@ -39,7 +37,7 @@ type githubWebhookRepository struct {
|
||||
config *provisioning.Repository
|
||||
owner string
|
||||
repo string
|
||||
secrets secrets.RepositorySecrets
|
||||
secret common.RawSecureValue
|
||||
gh pgh.Client
|
||||
webhookURL string
|
||||
}
|
||||
@@ -47,7 +45,7 @@ type githubWebhookRepository struct {
|
||||
func NewGithubWebhookRepository(
|
||||
basic pgh.GithubRepository,
|
||||
webhookURL string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
secret common.RawSecureValue,
|
||||
) GithubWebhookRepository {
|
||||
return &githubWebhookRepository{
|
||||
GithubRepository: basic,
|
||||
@@ -56,7 +54,7 @@ func NewGithubWebhookRepository(
|
||||
repo: basic.Repo(),
|
||||
gh: basic.Client(),
|
||||
webhookURL: webhookURL,
|
||||
secrets: secrets,
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +64,11 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request
|
||||
return nil, fmt.Errorf("unexpected webhook request")
|
||||
}
|
||||
|
||||
secret, err := r.secrets.Decrypt(ctx, r.config, string(r.config.Status.Webhook.EncryptedSecret))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt secret: %w", err)
|
||||
if r.secret.IsZero() {
|
||||
return nil, fmt.Errorf("missing webhook secret")
|
||||
}
|
||||
|
||||
payload, err := github.ValidatePayload(req, secret)
|
||||
payload, err := github.ValidatePayload(req, []byte(r.secret))
|
||||
if err != nil {
|
||||
return nil, apierrors.NewUnauthorized("invalid signature")
|
||||
}
|
||||
@@ -239,8 +236,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("get webhook: %w", err)
|
||||
}
|
||||
|
||||
hook.Secret = r.config.Status.Webhook.Secret // we always random gen this, so don't use it for mustUpdate below.
|
||||
|
||||
var mustUpdate bool
|
||||
|
||||
if hook.URL != r.webhookURL {
|
||||
@@ -248,6 +243,7 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
hook.URL = r.webhookURL
|
||||
}
|
||||
|
||||
slices.Sort(hook.Events) // consistent order for comparison
|
||||
if !slices.Equal(hook.Events, subscribedEvents) {
|
||||
mustUpdate = true
|
||||
hook.Events = subscribedEvents
|
||||
@@ -263,7 +259,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("could not generate secret: %w", err)
|
||||
}
|
||||
hook.Secret = secret.String()
|
||||
|
||||
if err := r.gh.EditWebhook(ctx, r.owner, r.repo, hook); err != nil {
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("edit webhook: %w", err)
|
||||
}
|
||||
@@ -304,10 +299,16 @@ func (r *githubWebhookRepository) OnCreate(ctx context.Context) ([]map[string]in
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
Secret: hook.Secret,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/secure/webhookSecret",
|
||||
"value": map[string]string{
|
||||
"create": hook.Secret,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -316,42 +317,34 @@ func (r *githubWebhookRepository) OnUpdate(ctx context.Context) ([]map[string]in
|
||||
return nil, nil
|
||||
}
|
||||
ctx, _ = r.logger(ctx, "")
|
||||
hook, _, err := r.updateWebhook(ctx)
|
||||
if err != nil {
|
||||
hook, changed, err := r.updateWebhook(ctx)
|
||||
if err != nil || !changed {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []map[string]interface{}{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook",
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
Secret: hook.Secret,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
// update the webhook and secret
|
||||
return []map[string]any{{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook",
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
}, nil
|
||||
}, {
|
||||
"op": "replace",
|
||||
"path": "/secure/webhookSecret",
|
||||
"value": map[string]string{
|
||||
"create": hook.Secret,
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (r *githubWebhookRepository) OnDelete(ctx context.Context) error {
|
||||
ctx, logger := r.logger(ctx, "")
|
||||
if err := r.GithubRepository.OnDelete(ctx); err != nil {
|
||||
return fmt.Errorf("on delete from basic github repository: %w", err)
|
||||
}
|
||||
|
||||
if r.config.Status.Webhook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
secretName := r.config.Name + webhookSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete webhook secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted webhook secret", "secretName", secretName)
|
||||
|
||||
return r.deleteWebhook(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
)
|
||||
|
||||
func TestParseWebhooks(t *testing.T) {
|
||||
@@ -114,9 +116,7 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *provisioning.Repository
|
||||
webhookSecret string
|
||||
setupRequest func() *http.Request
|
||||
mockSetup func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets)
|
||||
expected *provisioning.WebhookResponse
|
||||
expectedError error
|
||||
}{
|
||||
@@ -138,30 +138,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
expectedError: fmt.Errorf("unexpected webhook request"),
|
||||
},
|
||||
{
|
||||
name: "secret decryption error",
|
||||
config: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", nil)
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return(nil, errors.New("decryption failed"))
|
||||
},
|
||||
expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"),
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
config: &provisioning.Repository{
|
||||
@@ -171,22 +147,15 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader("invalid payload"))
|
||||
req.Header.Set("X-Hub-Signature-256", "invalid")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: apierrors.NewUnauthorized("invalid signature"),
|
||||
},
|
||||
{
|
||||
@@ -198,12 +167,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -218,10 +184,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ping received",
|
||||
@@ -239,12 +201,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/feature",
|
||||
@@ -264,10 +223,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
},
|
||||
@@ -287,12 +242,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -312,10 +264,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Job: &provisioning.JobSpec{
|
||||
@@ -336,12 +284,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main"
|
||||
@@ -358,10 +303,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing repository in push event"),
|
||||
},
|
||||
{
|
||||
@@ -373,12 +314,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -398,10 +336,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("repository mismatch"),
|
||||
},
|
||||
{
|
||||
@@ -416,12 +350,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -441,10 +372,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
},
|
||||
@@ -461,12 +388,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -497,10 +421,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Message: "pull request: opened",
|
||||
@@ -528,12 +448,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "synchronize",
|
||||
@@ -564,10 +481,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Message: "pull request: synchronize",
|
||||
@@ -592,12 +505,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -628,10 +538,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ignoring pull request event as develop is not the configured branch",
|
||||
@@ -646,12 +552,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "closed",
|
||||
@@ -682,10 +585,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ignore pull request event: closed",
|
||||
@@ -700,12 +599,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -733,10 +629,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing repository in pull request event"),
|
||||
},
|
||||
{
|
||||
@@ -746,12 +638,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
// GitHub config is intentionally missing
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -782,10 +671,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing GitHub config"),
|
||||
},
|
||||
{
|
||||
@@ -797,12 +682,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -833,10 +715,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("repository mismatch"),
|
||||
},
|
||||
{
|
||||
@@ -848,12 +726,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -873,39 +748,8 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("expected PR in event"),
|
||||
},
|
||||
{
|
||||
name: "secret decryption error with new secrets store",
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", nil)
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return(nil, errors.New("decryption failed"))
|
||||
},
|
||||
expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"),
|
||||
},
|
||||
{
|
||||
name: "ping event with new secrets store",
|
||||
config: &provisioning.Repository{
|
||||
@@ -918,12 +762,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -938,10 +779,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ping received",
|
||||
@@ -963,12 +800,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -988,10 +822,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Job: &provisioning.JobSpec{
|
||||
@@ -1012,12 +842,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -1032,10 +859,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusNotImplemented,
|
||||
Message: "unsupported messageType: team",
|
||||
@@ -1045,20 +868,12 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a mock secrets service
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
|
||||
// Set up the mock expectations
|
||||
if tt.mockSetup != nil {
|
||||
tt.mockSetup(t, mockSecrets)
|
||||
}
|
||||
|
||||
// Create a GitHub repository with the test config
|
||||
repo := &githubWebhookRepository{
|
||||
config: tt.config,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
secrets: mockSecrets,
|
||||
config: tt.config,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
secret: common.RawSecureValue("webhook-secret"),
|
||||
}
|
||||
|
||||
// Call the Webhook method
|
||||
@@ -1070,7 +885,7 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
var statusErr *apierrors.StatusError
|
||||
if errors.As(tt.expectedError, &statusErr) {
|
||||
var actualStatusErr *apierrors.StatusError
|
||||
require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type")
|
||||
require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type: %T", err)
|
||||
require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message)
|
||||
require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code)
|
||||
} else {
|
||||
@@ -1097,9 +912,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
require.Nil(t, response.Job)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all mock expectations were met
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1202,9 +1014,8 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
Secret: "test-secret",
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
@@ -1270,12 +1081,21 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
if tt.expectedHook != nil {
|
||||
require.NotNil(t, hookOps)
|
||||
require.Len(t, hookOps, 1)
|
||||
require.Len(t, hookOps, 2)
|
||||
require.Equal(t, "replace", hookOps[0]["op"])
|
||||
require.Equal(t, "/status/webhook", hookOps[0]["path"])
|
||||
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
|
||||
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
|
||||
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
|
||||
|
||||
require.Equal(t, "replace", hookOps[1]["op"])
|
||||
require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"])
|
||||
vals, ok := hookOps[1]["value"].(map[string]string)
|
||||
require.True(t, ok, "expected webhookSecret as map")
|
||||
require.Len(t, vals, 1, "with one property")
|
||||
require.NotEmpty(t, vals["create"], "secret should be created")
|
||||
|
||||
_, err := uuid.Parse(vals["create"])
|
||||
require.NoError(t, err, "the secret is a valid UUID")
|
||||
} else {
|
||||
require.Nil(t, hookOps)
|
||||
}
|
||||
@@ -1607,7 +1427,6 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
URL: "https://example.com/webhook",
|
||||
Events: subscribedEvents,
|
||||
}, nil)
|
||||
|
||||
// No EditWebhook call expected since no changes needed
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
@@ -1618,19 +1437,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
Secret: "secret",
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: common.InlineSecureValue{
|
||||
Name: "valid-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
SubscribedEvents: subscribedEvents,
|
||||
Secret: "secret",
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: nil, // nothing changed
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
@@ -1662,17 +1480,22 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
if tt.expectedHook != nil {
|
||||
require.NotNil(t, hookOps)
|
||||
require.Len(t, hookOps, 1)
|
||||
require.Len(t, hookOps, 2)
|
||||
require.Equal(t, "replace", hookOps[0]["op"])
|
||||
require.Equal(t, "/status/webhook", hookOps[0]["path"])
|
||||
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
|
||||
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
|
||||
if tt.expectedHook.Secret != "" {
|
||||
require.Equal(t, tt.expectedHook.Secret, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret)
|
||||
} else {
|
||||
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
|
||||
}
|
||||
require.ElementsMatch(t, tt.expectedHook.SubscribedEvents, hookOps[0]["value"].(*provisioning.WebhookStatus).SubscribedEvents)
|
||||
|
||||
require.Equal(t, "replace", hookOps[1]["op"])
|
||||
require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"])
|
||||
vals, ok := hookOps[1]["value"].(map[string]string)
|
||||
require.True(t, ok, "expected webhookSecret as map")
|
||||
require.Len(t, vals, 1, "with one property")
|
||||
require.NotEmpty(t, vals["create"], "secret should be created")
|
||||
|
||||
_, err := uuid.Parse(vals["create"])
|
||||
require.NoError(t, err, "the secret is a valid UUID")
|
||||
} else {
|
||||
require.Nil(t, hookOps)
|
||||
}
|
||||
@@ -1687,17 +1510,14 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets)
|
||||
setupMock func(m *github.MockClient)
|
||||
config *provisioning.Repository
|
||||
webhookURL string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "successfully delete webhook",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
// Mock deleting the webhook
|
||||
setupMock: func(m *github.MockClient) {
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
|
||||
Return(nil)
|
||||
},
|
||||
@@ -1721,10 +1541,8 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "no webhook URL provided",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
},
|
||||
name: "no webhook URL provided",
|
||||
setupMock: func(_ *github.MockClient) {},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
@@ -1740,8 +1558,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "webhook not found in status",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
setupMock: func(_ *github.MockClient) {
|
||||
// No secrets deletion or webhook deletion mocks needed - method returns early when webhook is nil
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
@@ -1760,35 +1577,9 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: nil, // No error expected - method returns early when webhook is nil
|
||||
},
|
||||
{
|
||||
name: "error on delete from basic github repository",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(fmt.Errorf("failed to delete webhook"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: fmt.Errorf("on delete from basic github repository: failed to delete webhook"),
|
||||
},
|
||||
{
|
||||
name: "error deleting webhook",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
setupMock: func(m *github.MockClient) {
|
||||
// Mock webhook deletion failure
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
|
||||
Return(fmt.Errorf("failed to delete webhook"))
|
||||
@@ -1819,15 +1610,13 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
// Setup mock GitHub client
|
||||
mockGH := github.NewMockClient(t)
|
||||
mockRepo := github.NewMockGithubRepository(t)
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockGH, mockRepo, mockSecrets)
|
||||
tt.setupMock(mockGH)
|
||||
|
||||
// Create repository with mock
|
||||
repo := &githubWebhookRepository{
|
||||
GithubRepository: mockRepo,
|
||||
gh: mockGH,
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
webhookURL: tt.webhookURL,
|
||||
@@ -1849,156 +1638,3 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubRepository_OnDelete_WithSecrets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
webhookURL string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful deletion with secrets",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(
|
||||
mock.Anything,
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+webhookSecretSuffix,
|
||||
).Return(nil)
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(
|
||||
mock.Anything,
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+webhookSecretSuffix,
|
||||
).Return(errors.New("failed to delete webhook secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: "delete webhook secret: failed to delete webhook secret",
|
||||
},
|
||||
{
|
||||
name: "no webhook URL - no secrets deletion",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockClient := github.NewMockClient(t)
|
||||
mockRepo := github.NewMockGithubRepository(t)
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockClient, mockRepo, mockSecrets)
|
||||
|
||||
repo := &githubWebhookRepository{
|
||||
GithubRepository: mockRepo,
|
||||
gh: mockClient,
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
webhookURL: tt.webhookURL,
|
||||
}
|
||||
|
||||
err := repo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
mockRepo.AssertExpectations(t)
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func (s *webhookConnector) updateLastEvent(ctx context.Context, repo repository.
|
||||
eventAge := time.Since(lastEvent)
|
||||
|
||||
if repo.Config().Status.Webhook != nil && (eventAge > time.Minute) {
|
||||
patchOp := map[string]interface{}{
|
||||
patchOp := map[string]any{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook/lastEvent",
|
||||
"value": time.Now().UnixMilli(),
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/service"
|
||||
@@ -45,7 +44,6 @@ var WireSet = wire.NewSet(
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
iam.RegisterAPIService,
|
||||
secrets.ProvideRepositorySecrets,
|
||||
provisioning.RegisterAPIService,
|
||||
service.RegisterAPIService,
|
||||
query.RegisterAPIService,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -37,7 +36,6 @@ func ProvideTestEnv(
|
||||
idService auth.IDService,
|
||||
githubFactory *github.Factory,
|
||||
decryptService secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets, // TODO... remove
|
||||
) (*TestEnv, error) {
|
||||
return &TestEnv{
|
||||
TestingT: testingT,
|
||||
@@ -54,7 +52,6 @@ func ProvideTestEnv(
|
||||
IDService: idService,
|
||||
GitHubFactory: githubFactory,
|
||||
DecryptService: decryptService,
|
||||
RepositorySecrets: repositorySecrets, // TODO, remove
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,5 +74,4 @@ type TestEnv struct {
|
||||
IDService auth.IDService
|
||||
GitHubFactory *github.Factory
|
||||
DecryptService secret.DecryptService
|
||||
RepositorySecrets secrets.RepositorySecrets // NOTE, this will be removed soon
|
||||
}
|
||||
|
||||
+7
-12
File diff suppressed because one or more lines are too long
@@ -315,13 +315,6 @@ var (
|
||||
RequiresRestart: true,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "provisioningSecretsService",
|
||||
Description: "Experimental feature to use the secrets service for provisioning instead of the legacy secrets",
|
||||
Stage: FeatureStageExperimental,
|
||||
RequiresRestart: true,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "grafanaAPIServerEnsureKubectlAccess",
|
||||
Description: "Start an additional https handler and write kubectl options",
|
||||
|
||||
@@ -40,7 +40,6 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false
|
||||
datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
provisioningSecretsService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false
|
||||
awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false
|
||||
|
||||
|
@@ -171,10 +171,6 @@ const (
|
||||
// Next generation provisioning... and git
|
||||
FlagProvisioning = "provisioning"
|
||||
|
||||
// FlagProvisioningSecretsService
|
||||
// Experimental feature to use the secrets service for provisioning instead of the legacy secrets
|
||||
FlagProvisioningSecretsService = "provisioningSecretsService"
|
||||
|
||||
// FlagGrafanaAPIServerEnsureKubectlAccess
|
||||
// Start an additional https handler and write kubectl options
|
||||
FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess"
|
||||
|
||||
@@ -2640,7 +2640,8 @@
|
||||
"metadata": {
|
||||
"name": "provisioningSecretsService",
|
||||
"resourceVersion": "1753448760331",
|
||||
"creationTimestamp": "2025-07-25T13:06:00Z"
|
||||
"creationTimestamp": "2025-07-25T13:06:00Z",
|
||||
"deletionTimestamp": "2025-08-20T12:48:19Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets",
|
||||
|
||||
@@ -2605,20 +2605,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenUser": {
|
||||
"description": "TokenUser is the user that will be used to access the repository if it's a personal access token.",
|
||||
"type": "string"
|
||||
@@ -2761,12 +2751,6 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"generateDashboardPreviews": {
|
||||
"description": "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).",
|
||||
"type": "boolean"
|
||||
@@ -2775,10 +2759,6 @@
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "The repository URL (e.g. `https://github.com/example/test`).",
|
||||
"type": "string"
|
||||
@@ -2796,20 +2776,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "The repository URL (e.g. `https://gitlab.com/example/test`).",
|
||||
"type": "string"
|
||||
@@ -2827,20 +2797,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenUser": {
|
||||
"description": "TokenUser is the user that will be used to access the repository if it's a personal access token.",
|
||||
"type": "string"
|
||||
@@ -4213,7 +4173,6 @@
|
||||
]
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SecureValues": {
|
||||
"description": "NOT YET USED FOR REAL -- testing secure value workflow",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
@@ -4226,7 +4185,7 @@
|
||||
]
|
||||
},
|
||||
"webhookSecret": {
|
||||
"description": "Some webhooks (github) require a secret key value",
|
||||
"description": "Some webhooks (including github) require a secret key value",
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4422,10 +4381,6 @@
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.WebhookStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encryptedSecret": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -4434,9 +4389,6 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscribedEvents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -570,17 +570,9 @@ func withLogs(opts *testinfra.GrafanaOpts) {
|
||||
opts.EnableLog = true
|
||||
}
|
||||
|
||||
func useAppPlatformSecrets(opts *testinfra.GrafanaOpts) {
|
||||
opts.EnableFeatureToggles = append(opts.EnableFeatureToggles,
|
||||
featuremgmt.FlagProvisioningSecretsService,
|
||||
featuremgmt.FlagSecretsManagementAppPlatform,
|
||||
)
|
||||
}
|
||||
|
||||
func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper {
|
||||
provisioningPath := t.TempDir()
|
||||
opts := testinfra.GrafanaOpts{
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagProvisioning,
|
||||
},
|
||||
|
||||
@@ -49,12 +49,12 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
// Move encrypted token mutation
|
||||
token, found, err := unstructured.NestedString(output.Object, "spec", "github", "encryptedToken")
|
||||
require.NoError(t, err, "encryptedToken is not a string")
|
||||
token, found, err := unstructured.NestedString(output.Object, "secure", "token", "name")
|
||||
require.NoError(t, err, "secure token name is not a string")
|
||||
if found {
|
||||
unstructured.RemoveNestedField(input.Object, "spec", "github", "token")
|
||||
err = unstructured.SetNestedField(input.Object, token, "spec", "github", "encryptedToken")
|
||||
require.NoError(t, err, "unable to copy encrypted token")
|
||||
require.True(t, strings.HasPrefix("inline-", token)) // name created automatically
|
||||
err = unstructured.SetNestedField(input.Object, token, "secure", "token", "name")
|
||||
require.NoError(t, err, "unable to copy secure token")
|
||||
}
|
||||
|
||||
// Marshal as real objects to ",omitempty" values are tested properly
|
||||
|
||||
@@ -2,9 +2,6 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,10 +10,6 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
@@ -24,12 +17,12 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
decryptService := helper.GetEnv().DecryptService
|
||||
require.NotNil(t, decryptService, "decrypt service wired properly")
|
||||
require.NotNil(t, decryptService, "decrypt service not wired properly")
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
@@ -107,519 +100,3 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_LegacySecrets(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "github token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Move encrypted token mutation
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name")
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_LegacyUpdate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
updateOptions := metav1.UpdateOptions{}
|
||||
ctx := context.Background()
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
updateValues map[string]any
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "update github token (legacy secrets)",
|
||||
values: map[string]any{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
updateValues: map[string]any{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update git token (legacy secrets)",
|
||||
values: map[string]any{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
updateValues: map[string]any{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
|
||||
// Prepare updated resource
|
||||
updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues)
|
||||
// Set the same name and resourceVersion for update
|
||||
updatedInput.Object["metadata"].(map[string]any)["name"] = name
|
||||
|
||||
// Fetch current resourceVersion
|
||||
current, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to get current resource for update")
|
||||
updatedInput.Object["metadata"].(map[string]any)["resourceVersion"] = current.Object["metadata"].(map[string]any)["resourceVersion"]
|
||||
|
||||
_, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions)
|
||||
require.NoError(t, err, "failed to update resource")
|
||||
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource after update")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name")
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
// TODO: Add test of fallbacks
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "github token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Move encrypted token mutation
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
|
||||
if expectedField.ExpectedValue != "" {
|
||||
require.Equal(t, name+"-"+expectedField.ExpectedValue, value)
|
||||
}
|
||||
|
||||
if expectedField.ExpectedDecryptedValue != "" {
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_Update(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
ctx := context.Background()
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
createOptions := metav1.CreateOptions{}
|
||||
updateOptions := metav1.UpdateOptions{}
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
values map[string]interface{}
|
||||
updateValues map[string]interface{}
|
||||
expectedFields []expectedField
|
||||
updatedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "update encrypted git token",
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
updateValues: map[string]interface{}{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "initial-token",
|
||||
},
|
||||
},
|
||||
updatedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update encrypted github token",
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
updateValues: map[string]interface{}{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "initial-token",
|
||||
},
|
||||
},
|
||||
updatedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
// Update the resource
|
||||
updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues)
|
||||
// Set the same name and resourceVersion for update
|
||||
_ = unstructured.SetNestedField(updatedInput.Object, name, "metadata", "name")
|
||||
_ = unstructured.SetNestedField(updatedInput.Object, output.GetResourceVersion(), "metadata", "resourceVersion")
|
||||
_, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions)
|
||||
require.NoError(t, err, "failed to update resource")
|
||||
|
||||
updatedOutput, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back updated resource")
|
||||
updatedRepo := unstructuredToRepository(t, updatedOutput)
|
||||
|
||||
// Check updated fields
|
||||
for _, expectedField := range test.updatedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, updatedRepo, updatedOutput.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
|
||||
if expectedField.ExpectedValue != "" {
|
||||
require.Equal(t, name+"-"+expectedField.ExpectedValue, value)
|
||||
}
|
||||
|
||||
if expectedField.ExpectedDecryptedValue != "" {
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_Removal(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
createOptions := metav1.CreateOptions{}
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
values map[string]interface{}
|
||||
expectedFields []expectedField
|
||||
updatedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "remove encrypted git token",
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove encrypted github token",
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Set the same name and resourceVersion for update
|
||||
err = helper.Repositories.Resource.Delete(ctx, name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err, "failed to delete resource")
|
||||
|
||||
for _, expectedField := range test.expectedFields {
|
||||
secretName, found, err := base64DecodedField(output.Object, expectedField.Path)
|
||||
require.NoError(t, err, "failed to decode base64 value")
|
||||
require.True(t, found, "secretName should be found")
|
||||
require.NotEmpty(t, secretName)
|
||||
|
||||
var lastDecrypted []byte
|
||||
require.Eventually(t, func() bool {
|
||||
lastDecrypted, err = secretsService.Decrypt(ctx, repo, secretName)
|
||||
return err != nil && errors.Is(err, contracts.ErrDecryptNotFound)
|
||||
}, 1000*time.Second, 500*time.Millisecond, "expected ErrDecryptNotFound error, got %v", lastDecrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func encryptedField(t *testing.T, secretsService secrets.RepositorySecrets, repo *provisioning.Repository, obj map[string]any, path []string, expectedValue bool) (string, string) {
|
||||
value, found, err := base64DecodedField(obj, path)
|
||||
if err != nil {
|
||||
require.NoError(t, err, "failed to decode base64 value")
|
||||
}
|
||||
|
||||
if expectedValue {
|
||||
decrypted, err := secretsService.Decrypt(context.Background(), repo, value)
|
||||
require.NoError(t, err, "failed to eecrypt value")
|
||||
return value, string(decrypted)
|
||||
} else {
|
||||
require.False(t, found, "value should not be found")
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
func base64DecodedField(obj map[string]any, path []string) (string, bool, error) {
|
||||
value, found, err := unstructured.NestedFieldNoCopy(obj, path...)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
valueStr, ok := value.(string)
|
||||
if !ok {
|
||||
return "", false, fmt.Errorf("value is not a string")
|
||||
}
|
||||
|
||||
decodedValue, err := base64.StdEncoding.DecodeString(valueStr)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("failed to decode base64 valueStr: %w", err)
|
||||
}
|
||||
|
||||
return string(decodedValue), true, nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"git": {
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
},
|
||||
"sync": {
|
||||
@@ -20,5 +19,8 @@
|
||||
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }}
|
||||
},
|
||||
"workflows": []
|
||||
},
|
||||
"secure": {
|
||||
"token": { "create": "{{ or .Token "" }}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
},
|
||||
"sync": {
|
||||
@@ -21,5 +20,8 @@
|
||||
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }}
|
||||
},
|
||||
"workflows": []
|
||||
},
|
||||
"secure": {
|
||||
"token": { "create": "{{ or .Token "" }}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -938,20 +938,16 @@ export type InlineSecureValue =
|
||||
export type SecureValues = {
|
||||
/** Token used to connect the configured repository */
|
||||
token?: InlineSecureValue;
|
||||
/** Some webhooks (github) require a secret key value */
|
||||
/** Some webhooks (including github) require a secret key value */
|
||||
webhookSecret?: InlineSecureValue;
|
||||
};
|
||||
export type BitbucketRepositoryConfig = {
|
||||
/** The branch to use in the repository. */
|
||||
branch: string;
|
||||
/** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */
|
||||
encryptedToken?: string;
|
||||
/** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.
|
||||
|
||||
When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */
|
||||
path?: string;
|
||||
/** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */
|
||||
token?: string;
|
||||
/** TokenUser is the user that will be used to access the repository if it's a personal access token. */
|
||||
tokenUser?: string;
|
||||
/** The repository URL (e.g. `https://bitbucket.org/example/test`). */
|
||||
@@ -960,14 +956,10 @@ export type BitbucketRepositoryConfig = {
|
||||
export type GitRepositoryConfig = {
|
||||
/** The branch to use in the repository. */
|
||||
branch: string;
|
||||
/** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */
|
||||
encryptedToken?: string;
|
||||
/** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.
|
||||
|
||||
When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */
|
||||
path?: string;
|
||||
/** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */
|
||||
token?: string;
|
||||
/** TokenUser is the user that will be used to access the repository if it's a personal access token. */
|
||||
tokenUser?: string;
|
||||
/** The repository URL (e.g. `https://github.com/example/test.git`). */
|
||||
@@ -976,30 +968,22 @@ export type GitRepositoryConfig = {
|
||||
export type GitHubRepositoryConfig = {
|
||||
/** The branch to use in the repository. */
|
||||
branch: string;
|
||||
/** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */
|
||||
encryptedToken?: string;
|
||||
/** Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews). */
|
||||
generateDashboardPreviews?: boolean;
|
||||
/** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.
|
||||
|
||||
When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */
|
||||
path?: string;
|
||||
/** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */
|
||||
token?: string;
|
||||
/** The repository URL (e.g. `https://github.com/example/test`). */
|
||||
url?: string;
|
||||
};
|
||||
export type GitLabRepositoryConfig = {
|
||||
/** The branch to use in the repository. */
|
||||
branch: string;
|
||||
/** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */
|
||||
encryptedToken?: string;
|
||||
/** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.
|
||||
|
||||
When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */
|
||||
path?: string;
|
||||
/** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */
|
||||
token?: string;
|
||||
/** The repository URL (e.g. `https://gitlab.com/example/test`). */
|
||||
url?: string;
|
||||
};
|
||||
@@ -1092,10 +1076,8 @@ export type SyncStatus = {
|
||||
state: 'error' | 'pending' | 'success' | 'warning' | 'working';
|
||||
};
|
||||
export type WebhookStatus = {
|
||||
encryptedSecret?: string;
|
||||
id?: number;
|
||||
lastEvent?: number;
|
||||
secret?: string;
|
||||
subscribedEvents?: string[];
|
||||
url?: string;
|
||||
};
|
||||
|
||||
@@ -78,8 +78,8 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
const onSubmit = async (form: RepositoryFormData) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const spec = dataToSpec(form, data);
|
||||
await submitData(spec);
|
||||
const spec = dataToSpec(form);
|
||||
await submitData(spec, form.token);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
|
||||
try {
|
||||
const formData = getValues();
|
||||
const spec = dataToSpec(formData.repository);
|
||||
const rsp = await submitData(spec);
|
||||
const rsp = await submitData(spec, formData.repository.token);
|
||||
if (rsp.error) {
|
||||
setStepStatusInfo({
|
||||
status: 'error',
|
||||
|
||||
@@ -13,7 +13,9 @@ export function useCreateOrUpdateRepository(name?: string) {
|
||||
const [testConfig, testRequest] = useCreateRepositoryTestMutation();
|
||||
|
||||
const updateOrCreate = useCallback(
|
||||
async (data: RepositorySpec) => {
|
||||
async (data: RepositorySpec, token?: string) => {
|
||||
const secure = token?.length ? { token: { create: token } } : undefined;
|
||||
|
||||
// First test the config and wait for the result
|
||||
// unwrap will throw an error if the test fails
|
||||
await testConfig({
|
||||
@@ -21,6 +23,7 @@ export function useCreateOrUpdateRepository(name?: string) {
|
||||
name: name || 'new',
|
||||
body: {
|
||||
spec: data,
|
||||
secure,
|
||||
},
|
||||
}).unwrap();
|
||||
|
||||
@@ -36,10 +39,11 @@ export function useCreateOrUpdateRepository(name?: string) {
|
||||
finalizers: ['cleanup', 'remove-orphan-resources'],
|
||||
},
|
||||
spec: data,
|
||||
secure,
|
||||
},
|
||||
});
|
||||
}
|
||||
return create({ repository: { metadata: generateRepositoryMetadata(data), spec: data } });
|
||||
return create({ repository: { metadata: generateRepositoryMetadata(data), spec: data, secure } });
|
||||
},
|
||||
[create, name, update, testConfig]
|
||||
);
|
||||
|
||||
@@ -44,6 +44,8 @@ export type RepositoryFormData = Omit<RepositorySpec, 'workflows' | RepositorySp
|
||||
LocalRepositoryConfig & {
|
||||
readOnly: boolean;
|
||||
prWorkflow: boolean;
|
||||
// top-level inline secure value
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type RepositorySettingsField = Path<RepositoryFormData>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Repository, RepositorySpec } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { RepositorySpec } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { RepositoryFormData } from '../types';
|
||||
|
||||
@@ -15,7 +15,7 @@ const getWorkflows = (data: RepositoryFormData): RepositorySpec['workflows'] =>
|
||||
return [...workflows, 'branch'];
|
||||
};
|
||||
|
||||
export const dataToSpec = (data: RepositoryFormData, existingRepository?: Repository): RepositorySpec => {
|
||||
export const dataToSpec = (data: RepositoryFormData): RepositorySpec => {
|
||||
const spec: RepositorySpec = {
|
||||
type: data.type,
|
||||
sync: data.sync,
|
||||
@@ -26,9 +26,7 @@ export const dataToSpec = (data: RepositoryFormData, existingRepository?: Reposi
|
||||
const baseConfig = {
|
||||
url: data.url || '',
|
||||
branch: data.branch,
|
||||
token: data.token,
|
||||
path: data.path,
|
||||
encryptedToken: data.encryptedToken,
|
||||
};
|
||||
|
||||
switch (data.type) {
|
||||
@@ -37,27 +35,12 @@ export const dataToSpec = (data: RepositoryFormData, existingRepository?: Reposi
|
||||
...baseConfig,
|
||||
generateDashboardPreviews: data.generateDashboardPreviews,
|
||||
};
|
||||
// Handle token merging for existing repositories
|
||||
if (existingRepository?.spec?.github) {
|
||||
spec.github.token = data.token || existingRepository.spec.github.token;
|
||||
spec.github.encryptedToken = existingRepository.spec.github.encryptedToken;
|
||||
}
|
||||
break;
|
||||
case 'gitlab':
|
||||
spec.gitlab = baseConfig;
|
||||
// Handle token merging for existing repositories
|
||||
if (existingRepository?.spec?.gitlab) {
|
||||
spec.gitlab.token = data.token || existingRepository.spec.gitlab.token;
|
||||
spec.gitlab.encryptedToken = existingRepository.spec.gitlab.encryptedToken;
|
||||
}
|
||||
break;
|
||||
case 'bitbucket':
|
||||
spec.bitbucket = baseConfig;
|
||||
// Handle token merging for existing repositories
|
||||
if (existingRepository?.spec?.bitbucket) {
|
||||
spec.bitbucket.token = data.token || existingRepository.spec.bitbucket.token;
|
||||
spec.bitbucket.encryptedToken = existingRepository.spec.bitbucket.encryptedToken;
|
||||
}
|
||||
break;
|
||||
case 'git':
|
||||
spec.git = baseConfig;
|
||||
|
||||
@@ -17,16 +17,13 @@ export const getFormErrors = (errors: ErrorDetails[]): FormErrorTuple => {
|
||||
'local.path',
|
||||
'github.branch',
|
||||
'github.url',
|
||||
'github.token',
|
||||
'secure.token',
|
||||
'gitlab.branch',
|
||||
'gitlab.url',
|
||||
'gitlab.token',
|
||||
'bitbucket.branch',
|
||||
'bitbucket.url',
|
||||
'bitbucket.token',
|
||||
'git.branch',
|
||||
'git.url',
|
||||
'git.token',
|
||||
];
|
||||
const fieldMap: Record<string, RepositoryFormPath> = {
|
||||
path: 'repository.path',
|
||||
|
||||
Reference in New Issue
Block a user