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:
Ryan McKinley
2025-08-22 18:38:28 +02:00
committed by GitHub
co-authored by Clarity-89 Roberto Jimenez Sanchez
parent 04f392d37b
commit ce65391067
69 changed files with 694 additions and 4255 deletions
+1
View File
@@ -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
+17
View File
@@ -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"},
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
-1
View File
@@ -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
View File
@@ -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)
}
+5 -4
View File
@@ -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
}
+62 -63
View File
@@ -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
}
+17 -8
View File
@@ -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
+1 -1
View File
@@ -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(),
-2
View File
@@ -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,
-4
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
-7
View File
@@ -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",
-1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
40 datasourceAPIServers experimental @grafana/grafana-app-platform-squad false true false
41 grafanaAPIServerWithExperimentalAPIs experimental @grafana/grafana-app-platform-squad true true false
42 provisioning experimental @grafana/grafana-app-platform-squad false true false
provisioningSecretsService experimental @grafana/grafana-app-platform-squad false true false
43 grafanaAPIServerEnsureKubectlAccess experimental @grafana/grafana-app-platform-squad true true false
44 featureToggleAdminPage experimental @grafana/grafana-operator-experience-squad false true false
45 awsAsyncQueryCaching GA @grafana/aws-datasources false false false
-4
View File
@@ -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"
+2 -1
View File
@@ -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 -525
View File
@@ -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>;
+2 -19
View File
@@ -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',