mirror of
https://github.com/grafana/grafana.git
synced 2026-01-11 14:34:19 +08:00
Compare commits
18 Commits
ash/react-
...
authlib-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea8d434c9 | ||
|
|
cddb1f9fa6 | ||
|
|
d2c78f5799 | ||
|
|
4f3f9ebc04 | ||
|
|
1498970e74 | ||
|
|
1465b44d5a | ||
|
|
ee62baea2c | ||
|
|
1f20ca5a3d | ||
|
|
97b241d4ab | ||
|
|
466a27deff | ||
|
|
264131a390 | ||
|
|
7698970f22 | ||
|
|
bbaf91ed9c | ||
|
|
92464b2dc8 | ||
|
|
5fe192a893 | ||
|
|
380154707b | ||
|
|
217427e072 | ||
|
|
585d24dafa |
@@ -68,14 +68,14 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/smithy-go v1.23.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
||||
@@ -173,8 +173,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
|
||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
@@ -185,10 +185,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
@@ -209,8 +209,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJ
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
|
||||
@@ -106,14 +106,14 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
@@ -124,7 +124,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/smithy-go v1.23.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
|
||||
@@ -238,8 +238,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
|
||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
@@ -250,10 +250,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
@@ -280,14 +280,16 @@ github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:Pwbxovp
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
@@ -2321,6 +2323,8 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
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=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
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=
|
||||
|
||||
@@ -30,14 +30,14 @@ require (
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/smithy-go v1.23.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
|
||||
@@ -28,22 +28,22 @@ github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
|
||||
@@ -30,11 +30,22 @@ KeeperSpec: {
|
||||
}
|
||||
|
||||
#AWSConfig: {
|
||||
accessKeyID: #CredentialValue
|
||||
secretAccessKey: #CredentialValue
|
||||
region: string
|
||||
accessKey?: #AWSAccessKey
|
||||
assumeRole?: #AWSAssumeRole
|
||||
kmsKeyID?: string
|
||||
}
|
||||
|
||||
#AWSAccessKey: {
|
||||
accessKeyID: #CredentialValue
|
||||
secretAccessKey: #CredentialValue
|
||||
}
|
||||
|
||||
#AWSAssumeRole: {
|
||||
assumeRoleArn: string
|
||||
externalID: string
|
||||
}
|
||||
|
||||
#AzureConfig: {
|
||||
keyVaultName: string
|
||||
tenantID: string
|
||||
|
||||
@@ -4,14 +4,26 @@ package v1beta1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type KeeperAWSConfig struct {
|
||||
AccessKeyID KeeperCredentialValue `json:"accessKeyID"`
|
||||
SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"`
|
||||
KmsKeyID *string `json:"kmsKeyID,omitempty"`
|
||||
Region string `json:"region"`
|
||||
AccessKey *KeeperAWSAccessKey `json:"accessKey,omitempty"`
|
||||
AssumeRole *KeeperAWSAssumeRole `json:"assumeRole,omitempty"`
|
||||
KmsKeyID *string `json:"kmsKeyID,omitempty"`
|
||||
}
|
||||
|
||||
// NewKeeperAWSConfig creates a new KeeperAWSConfig object.
|
||||
func NewKeeperAWSConfig() *KeeperAWSConfig {
|
||||
return &KeeperAWSConfig{
|
||||
return &KeeperAWSConfig{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type KeeperAWSAccessKey struct {
|
||||
AccessKeyID KeeperCredentialValue `json:"accessKeyID"`
|
||||
SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
// NewKeeperAWSAccessKey creates a new KeeperAWSAccessKey object.
|
||||
func NewKeeperAWSAccessKey() *KeeperAWSAccessKey {
|
||||
return &KeeperAWSAccessKey{
|
||||
AccessKeyID: *NewKeeperCredentialValue(),
|
||||
SecretAccessKey: *NewKeeperCredentialValue(),
|
||||
}
|
||||
@@ -36,6 +48,17 @@ func NewKeeperCredentialValue() *KeeperCredentialValue {
|
||||
return &KeeperCredentialValue{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type KeeperAWSAssumeRole struct {
|
||||
AssumeRoleArn string `json:"assumeRoleArn"`
|
||||
ExternalID string `json:"externalID"`
|
||||
}
|
||||
|
||||
// NewKeeperAWSAssumeRole creates a new KeeperAWSAssumeRole object.
|
||||
func NewKeeperAWSAssumeRole() *KeeperAWSAssumeRole {
|
||||
return &KeeperAWSAssumeRole{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type KeeperAzureConfig struct {
|
||||
KeyVaultName string `json:"keyVaultName"`
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
AzureKeeperType KeeperType = "azure"
|
||||
GCPKeeperType KeeperType = "gcp"
|
||||
HashiCorpKeeperType KeeperType = "hashicorp"
|
||||
SystemKeeperType KeeperType = "system"
|
||||
)
|
||||
|
||||
func (kt KeeperType) String() string {
|
||||
@@ -20,9 +21,31 @@ func (kt KeeperType) String() string {
|
||||
|
||||
// KeeperConfig is an interface that all keeper config types must implement.
|
||||
type KeeperConfig interface {
|
||||
// Returns the name of the keeper
|
||||
GetName() string
|
||||
Type() KeeperType
|
||||
}
|
||||
|
||||
type NamedKeeperConfig[T interface {
|
||||
Type() KeeperType
|
||||
}] struct {
|
||||
Name string
|
||||
Cfg T
|
||||
}
|
||||
|
||||
func NewNamedKeeperConfig[T interface {
|
||||
Type() KeeperType
|
||||
}](keeperName string, cfg T) *NamedKeeperConfig[T] {
|
||||
return &NamedKeeperConfig[T]{Name: keeperName, Cfg: cfg}
|
||||
}
|
||||
|
||||
func (c *NamedKeeperConfig[T]) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
func (c *NamedKeeperConfig[T]) Type() KeeperType {
|
||||
return c.Cfg.Type()
|
||||
}
|
||||
|
||||
func (s *KeeperSpec) GetType() KeeperType {
|
||||
if s.Aws != nil {
|
||||
return AWSKeeperType
|
||||
@@ -43,7 +66,7 @@ func (s *KeeperSpec) GetType() KeeperType {
|
||||
type SystemKeeperConfig struct{}
|
||||
|
||||
func (*SystemKeeperConfig) Type() KeeperType {
|
||||
return "system"
|
||||
return SystemKeeperType
|
||||
}
|
||||
|
||||
func (s *KeeperAWSConfig) Type() KeeperType {
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper": schema_pkg_apis_secret_v1beta1_Keeper(ref),
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey": schema_pkg_apis_secret_v1beta1_KeeperAWSAccessKey(ref),
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole": schema_pkg_apis_secret_v1beta1_KeeperAWSAssumeRole(ref),
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig": schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref),
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig": schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref),
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue": schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref),
|
||||
@@ -79,7 +81,7 @@ func schema_pkg_apis_secret_v1beta1_Keeper(ref common.ReferenceCallback) common.
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
func schema_pkg_apis_secret_v1beta1_KeeperAWSAccessKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
@@ -97,6 +99,65 @@ func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback
|
||||
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"accessKeyID", "secretAccessKey"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_secret_v1beta1_KeeperAWSAssumeRole(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"assumeRoleArn": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"externalID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"assumeRoleArn", "externalID"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"region": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"accessKey": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey"),
|
||||
},
|
||||
},
|
||||
"assumeRole": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole"),
|
||||
},
|
||||
},
|
||||
"kmsKeyID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
@@ -104,11 +165,11 @@ func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"accessKeyID", "secretAccessKey"},
|
||||
Required: []string{"region"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"},
|
||||
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,11 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/
|
||||
cloudwatch-troubleshooting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/troubleshooting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/troubleshooting/
|
||||
---
|
||||
|
||||
# Amazon CloudWatch data source
|
||||
@@ -119,6 +124,7 @@ The following documents will help you get started working with the CloudWatch da
|
||||
- [CloudWatch query editor](ref:cloudwatch-query-editor)
|
||||
- [Templates and variables](ref:cloudwatch-template-variables)
|
||||
- [Configure AWS authentication](ref:cloudwatch-aws-authentication)
|
||||
- [Troubleshoot CloudWatch issues](ref:cloudwatch-troubleshooting)
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
|
||||
519
docs/sources/datasources/aws-cloudwatch/troubleshooting/index.md
Normal file
519
docs/sources/datasources/aws-cloudwatch/troubleshooting/index.md
Normal file
@@ -0,0 +1,519 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../data-sources/aws-cloudwatch/troubleshooting/
|
||||
description: Troubleshooting guide for the Amazon CloudWatch data source in Grafana
|
||||
keywords:
|
||||
- grafana
|
||||
- cloudwatch
|
||||
- aws
|
||||
- troubleshooting
|
||||
- errors
|
||||
- authentication
|
||||
- query
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Troubleshooting
|
||||
title: Troubleshoot Amazon CloudWatch data source issues
|
||||
weight: 500
|
||||
refs:
|
||||
configure-cloudwatch:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/configure/
|
||||
cloudwatch-aws-authentication:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/aws-authentication/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/aws-authentication/
|
||||
cloudwatch-template-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/template-variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/template-variables/
|
||||
cloudwatch-query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch/query-editor/
|
||||
private-data-source-connect:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
---
|
||||
|
||||
# Troubleshoot Amazon CloudWatch data source issues
|
||||
|
||||
This document provides solutions to common issues you may encounter when configuring or using the Amazon CloudWatch data source. For configuration instructions, refer to [Configure CloudWatch](ref:configure-cloudwatch).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
The data source health check validates both metrics and logs permissions. If your IAM policy only grants access to one of these (for example, metrics-only or logs-only), the health check displays a red status. However, the service you have permissions for is still usable—you can query metrics or logs based on whichever permissions are configured.
|
||||
{{< /admonition >}}
|
||||
|
||||
## Authentication errors
|
||||
|
||||
These errors occur when AWS credentials are invalid, missing, or don't have the required permissions.
|
||||
|
||||
### "Access Denied" or "Not authorized to perform this operation"
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Save & test fails with "Access Denied"
|
||||
- Queries return authorization errors
|
||||
- Namespaces, metrics, or dimensions don't load
|
||||
|
||||
**Possible causes and solutions:**
|
||||
|
||||
| Cause | Solution |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| IAM policy missing required permissions | Attach the appropriate IAM policy to your user or role. For metrics, you need `cloudwatch:ListMetrics`, `cloudwatch:GetMetricData`, and related permissions. For logs, you need `logs:DescribeLogGroups`, `logs:StartQuery`, `logs:GetQueryResults`, and related permissions. Refer to [Configure CloudWatch](ref:configure-cloudwatch) for complete policy examples. |
|
||||
| Incorrect access key or secret key | Verify the credentials in the AWS Console under **IAM** > **Users** > your user > **Security credentials**. Generate new credentials if necessary. |
|
||||
| Credentials have expired | For temporary credentials, generate new ones. For access keys, verify they haven't been deactivated or deleted. |
|
||||
| Wrong AWS region | Verify the default region in the data source configuration matches where your resources are located. |
|
||||
| Assume Role ARN is incorrect | Verify the role ARN format: `arn:aws:iam::<account-id>:role/<role-name>`. Check that the role exists in the AWS Console. |
|
||||
|
||||
### "Unable to assume role"
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Authentication fails when using Assume Role ARN
|
||||
- Error message references STS or AssumeRole
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the trust relationship on the IAM role allows the Grafana credentials to assume it.
|
||||
1. Check the trust policy includes the correct principal (the user or role running Grafana).
|
||||
1. If using an external ID, ensure it matches exactly in both the role's trust policy and the Grafana data source configuration.
|
||||
1. Verify the base credentials have the `sts:AssumeRole` permission.
|
||||
1. Check that the role ARN is correct and the role exists.
|
||||
|
||||
**Example trust policy:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::<your-account-id>:user/<grafana-user>"
|
||||
},
|
||||
"Action": "sts:AssumeRole",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"sts:ExternalId": "<your-external-id>"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### AWS SDK Default authentication not working
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Data source test fails when using AWS SDK Default
|
||||
- Works locally but fails in production
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify AWS credentials are configured in the environment where Grafana runs.
|
||||
1. Check for credentials in the default locations:
|
||||
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
||||
- Shared credentials file (`~/.aws/credentials`)
|
||||
- EC2 instance metadata (if running on EC2)
|
||||
- ECS task role (if running in ECS)
|
||||
- EKS service account (if running in EKS)
|
||||
1. Ensure the Grafana process has permission to read the credentials file.
|
||||
1. For EKS with IRSA, set the pod's security context to allow user 472 (grafana) to access the projected token. Refer to [AWS authentication](ref:cloudwatch-aws-authentication) for details.
|
||||
|
||||
### Credentials file not found
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Error indicates credentials file cannot be read
|
||||
- Authentication fails with "Credentials file" option
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Create the credentials file at `~/.aws/credentials` for the user running the `grafana-server` service.
|
||||
1. Verify the file has correct permissions (`0644`).
|
||||
1. If the file exists but isn't working, move it to `/usr/share/grafana/` and set permissions to `0644`.
|
||||
1. Ensure the profile name in the data source configuration matches a profile in the credentials file.
|
||||
|
||||
## Connection errors
|
||||
|
||||
These errors occur when Grafana cannot reach AWS CloudWatch endpoints.
|
||||
|
||||
### "Request timed out" or connection failures
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Data source test times out
|
||||
- Queries fail with timeout errors
|
||||
- Intermittent connection issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify network connectivity from the Grafana server to AWS endpoints.
|
||||
1. Check firewall rules allow outbound HTTPS (port 443) to AWS services.
|
||||
1. If using a VPC, ensure proper NAT gateway or VPC endpoint configuration.
|
||||
1. For Grafana Cloud connecting to private resources, configure [Private data source connect](ref:private-data-source-connect).
|
||||
1. Check if the default region is correct—incorrect regions may cause longer timeouts.
|
||||
1. Increase the timeout settings if queries involve large data volumes.
|
||||
|
||||
### Custom endpoint configuration issues
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Connection fails when using a custom endpoint
|
||||
- Endpoint URL rejected
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the endpoint URL format is correct.
|
||||
1. Ensure the endpoint is accessible from the Grafana server.
|
||||
1. Check that the endpoint supports the required AWS APIs.
|
||||
1. For VPC endpoints, verify the endpoint policy allows the required actions.
|
||||
|
||||
## CloudWatch Metrics query errors
|
||||
|
||||
These errors occur when querying CloudWatch Metrics.
|
||||
|
||||
### "No data" or empty results
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Query executes without error but returns no data
|
||||
- Charts show "No data" message
|
||||
|
||||
**Possible causes and solutions:**
|
||||
|
||||
| Cause | Solution |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| Time range doesn't contain data | Expand the dashboard time range. CloudWatch metrics have different retention periods based on resolution. |
|
||||
| Wrong namespace or metric name | Verify the namespace (for example, `AWS/EC2`) and metric name (for example, `CPUUtilization`) are correct. |
|
||||
| Incorrect dimensions | Ensure dimension names and values match your AWS resources exactly. |
|
||||
| Match Exact enabled incorrectly | When Match Exact is enabled, all dimensions must be specified. Try disabling it to see if metrics appear. |
|
||||
| Period too large | Reduce the period setting or set it to "auto" to ensure data points are returned for your time range. |
|
||||
| Custom metrics not configured | Add custom metric namespaces in the data source configuration under **Namespaces of Custom Metrics**. |
|
||||
|
||||
### "Metric not found" or metrics don't appear in drop-down
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Expected metrics don't appear in the query editor
|
||||
- Metric drop-down is empty for a namespace
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the metric exists in the selected region.
|
||||
1. For custom metrics, add the namespace to **Namespaces of Custom Metrics** in the data source configuration.
|
||||
1. Check that the IAM policy includes `cloudwatch:ListMetrics` permission.
|
||||
1. CloudWatch limits `ListMetrics` to 500 results per page. To retrieve more metrics, increase the `list_metrics_page_limit` setting in the [Grafana configuration file](https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/configure/#configure-the-data-source-with-grafanaini).
|
||||
1. Use the Query Inspector to verify the API request and response.
|
||||
|
||||
### Dimension values not loading
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Dimension value drop-down doesn't populate
|
||||
- Wildcard searches return no results
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the IAM policy includes `cloudwatch:ListMetrics` permission.
|
||||
1. Check that the namespace and metric are selected before dimension values can load.
|
||||
1. For EC2 dimensions, ensure `ec2:DescribeTags` and `ec2:DescribeInstances` permissions are granted.
|
||||
1. Dimension values require existing metrics—if no metrics match, no values appear.
|
||||
|
||||
### "Too many data points" or API throttling
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Queries fail with throttling errors
|
||||
- Performance degrades with multiple panels
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Increase the period setting to reduce the number of data points.
|
||||
1. Reduce the time range of your queries.
|
||||
1. Use fewer dimensions or wildcard queries per panel.
|
||||
1. Request a quota increase for `GetMetricData` requests per second in the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/).
|
||||
1. Enable query caching in Grafana to reduce API calls.
|
||||
|
||||
### Metric math expression errors
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Expression returns errors
|
||||
- Referenced metrics not found
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify each referenced metric has a unique ID set.
|
||||
1. Check that metric IDs start with a lowercase letter and contain only letters, numbers, and underscores.
|
||||
1. Ensure all referenced metrics are in the same query.
|
||||
1. Verify the expression syntax follows [AWS Metric Math](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) documentation.
|
||||
1. Metric math expressions can't be used with Grafana alerting if they reference other query rows.
|
||||
|
||||
## CloudWatch Logs query errors
|
||||
|
||||
These errors occur when querying CloudWatch Logs.
|
||||
|
||||
### "Query failed" or logs don't appear
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Log queries return errors
|
||||
- No log data is displayed
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify log group names are correct and exist in the selected region.
|
||||
1. Check the IAM policy includes `logs:StartQuery`, `logs:GetQueryResults`, and `logs:DescribeLogGroups` permissions.
|
||||
1. Ensure the time range contains log data.
|
||||
1. Verify the query syntax is valid. For CloudWatch Logs Insights QL, test the query in the AWS Console.
|
||||
1. Select the correct query language (Logs Insights QL, OpenSearch PPL, or OpenSearch SQL) based on your query syntax.
|
||||
|
||||
### Log query timeout
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Query runs for a long time then fails
|
||||
- Error mentions timeout
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Increase the **Query timeout result** setting in the data source configuration (default is 30 minutes).
|
||||
1. Narrow the time range to reduce the amount of data scanned.
|
||||
1. Add filters to your query to limit results.
|
||||
1. Break complex queries into smaller, more focused queries.
|
||||
1. For alerting, the timeout defined in the [Grafana configuration file](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#unified_alerting) takes precedence.
|
||||
|
||||
### Log groups not appearing in selector
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Log group selector is empty
|
||||
- Can't find expected log groups
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the IAM policy includes `logs:DescribeLogGroups` permission.
|
||||
1. Check that log groups exist in the selected region.
|
||||
1. For cross-account observability, ensure proper IAM permissions for `oam:ListSinks` and `oam:ListAttachedLinks`.
|
||||
1. Use prefix search to filter log groups if you have many groups.
|
||||
1. Verify the selected account (for cross-account) contains the expected log groups.
|
||||
|
||||
### OpenSearch SQL query errors
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- OpenSearch SQL queries fail
|
||||
- Syntax errors with SQL queries
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Specify the log group identifier or ARN in the `FROM` clause:
|
||||
|
||||
```sql
|
||||
SELECT * FROM `log_group_name` WHERE `@message` LIKE '%error%'
|
||||
```
|
||||
|
||||
1. For multiple log groups, use the `logGroups` function:
|
||||
|
||||
```sql
|
||||
SELECT * FROM `logGroups(logGroupIdentifier: ['LogGroup1', 'LogGroup2'])`
|
||||
```
|
||||
|
||||
1. Amazon CloudWatch supports only a subset of OpenSearch SQL commands. Refer to the [CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_Languages.html) for supported syntax.
|
||||
|
||||
## Template variable errors
|
||||
|
||||
These errors occur when using template variables with the CloudWatch data source.
|
||||
|
||||
### Variables return no values
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Variable drop-down is empty
|
||||
- Dashboard fails to load with variable errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the data source connection is working.
|
||||
1. Check that the IAM policy includes permissions for the variable query type:
|
||||
- **Regions:** No additional permissions needed.
|
||||
- **Namespaces:** No additional permissions needed.
|
||||
- **Metrics:** Requires `cloudwatch:ListMetrics`.
|
||||
- **Dimension Values:** Requires `cloudwatch:ListMetrics`.
|
||||
- **EC2 Instance Attributes:** Requires `ec2:DescribeInstances`.
|
||||
- **EBS Volume IDs:** Requires `ec2:DescribeVolumes`.
|
||||
- **Resource ARNs:** Requires `tag:GetResources`.
|
||||
- **Log Groups:** Requires `logs:DescribeLogGroups`.
|
||||
1. For dependent variables, ensure parent variables have valid selections.
|
||||
1. Verify the region is set correctly (use "default" for the data source's default region).
|
||||
|
||||
For more information on template variables, refer to [CloudWatch template variables](ref:cloudwatch-template-variables).
|
||||
|
||||
### Multi-value template variables cause query failures
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Queries fail when selecting multiple dimension values
|
||||
- Error about search expression limits
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Search expressions are limited to 1,024 characters. Reduce the number of selected values.
|
||||
1. Use the asterisk (`*`) wildcard instead of selecting "All" to query all metrics for a dimension.
|
||||
1. Multi-valued template variables are only supported for dimension values—not for Region, Namespace, or Metric Name.
|
||||
|
||||
## Cross-account observability errors
|
||||
|
||||
These errors occur when using CloudWatch cross-account observability features.
|
||||
|
||||
### Cross-account queries fail
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Can't query metrics or logs from linked accounts
|
||||
- Monitoring account badge doesn't appear
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify cross-account observability is configured in the AWS CloudWatch console.
|
||||
1. Add the required IAM permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": ["oam:ListSinks", "oam:ListAttachedLinks"],
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1. Check that the monitoring account and source accounts are properly linked in AWS.
|
||||
1. Cross-account observability works within a single region—verify all accounts are in the same region.
|
||||
1. EC2 Instance Attributes can't be queried across accounts because they use the EC2 API, not the CloudWatch API.
|
||||
|
||||
## Quota and pricing issues
|
||||
|
||||
These issues relate to AWS service quotas and cost management.
|
||||
|
||||
### API throttling errors
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- "Rate exceeded" errors
|
||||
- Dashboard panels intermittently fail to load
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Reduce the frequency of dashboard refreshes.
|
||||
1. Increase the period setting to reduce `GetMetricData` requests.
|
||||
1. Enable query caching in Grafana (available in Grafana Enterprise and Grafana Cloud).
|
||||
1. Request a quota increase in the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/).
|
||||
1. Consider consolidating similar queries using metric math.
|
||||
|
||||
### Unexpectedly high CloudWatch costs
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- AWS CloudWatch costs are higher than expected
|
||||
- Frequent API calls from Grafana
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. The `GetMetricData` API doesn't qualify for the CloudWatch API free tier.
|
||||
1. Reduce dashboard auto-refresh frequency.
|
||||
1. Increase the period setting to reduce data points returned.
|
||||
1. Use query caching to reduce repeated API calls.
|
||||
1. Review variable query settings—set variable refresh to "On dashboard load" instead of "On time range change."
|
||||
1. Avoid using wildcards in dimensions when possible, as they generate search expressions with multiple API calls.
|
||||
|
||||
## Other common issues
|
||||
|
||||
These issues don't produce specific error messages but are commonly encountered.
|
||||
|
||||
### Custom metrics don't appear
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Custom metrics from applications or agents don't show in the namespace drop-down
|
||||
- Only standard AWS namespaces are visible
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Add your custom metric namespace to the **Namespaces of Custom Metrics** field in the data source configuration.
|
||||
1. Separate multiple namespaces with commas (for example, `CWAgent,CustomNamespace`).
|
||||
1. Verify custom metrics have been published to CloudWatch in the selected region.
|
||||
|
||||
### Pre-configured dashboards not working
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Imported dashboards show no data
|
||||
- Dashboard variables don't load
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the data source name in the dashboard matches your CloudWatch data source.
|
||||
1. Check that the dashboard's AWS region setting matches where your resources are located.
|
||||
1. Ensure the IAM policy grants access to the required services (EC2, Lambda, RDS, etc.).
|
||||
1. Verify resources exist and are emitting metrics in the selected region.
|
||||
|
||||
### X-Ray trace links not appearing
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Log entries don't show X-Ray trace links
|
||||
- `@xrayTraceId` field not appearing
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify an X-Ray data source is configured and linked in the CloudWatch data source settings.
|
||||
1. Ensure your logs contain the `@xrayTraceId` field.
|
||||
1. Update log queries to include `@xrayTraceId` in the fields, for example: `fields @message, @xrayTraceId`.
|
||||
1. Configure your application to log X-Ray trace IDs. Refer to the [AWS X-Ray documentation](https://docs.aws.amazon.com/xray/latest/devguide/xray-services.html).
|
||||
|
||||
## Enable debug logging
|
||||
|
||||
To capture detailed error information for troubleshooting:
|
||||
|
||||
1. Set the Grafana log level to `debug` in the configuration file:
|
||||
|
||||
```ini
|
||||
[log]
|
||||
level = debug
|
||||
```
|
||||
|
||||
1. Review logs in `/var/log/grafana/grafana.log` (or your configured log location).
|
||||
1. Look for CloudWatch-specific entries that include request and response details.
|
||||
1. Reset the log level to `info` after troubleshooting to avoid excessive log volume.
|
||||
|
||||
## Get additional help
|
||||
|
||||
If you've tried the solutions above and still encounter issues:
|
||||
|
||||
1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues.
|
||||
1. Review the [CloudWatch plugin GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
|
||||
1. Consult the [AWS CloudWatch documentation](https://docs.aws.amazon.com/cloudwatch/) for service-specific guidance.
|
||||
1. Contact Grafana Support if you're an Enterprise, Cloud Pro, or Cloud Contracted user.
|
||||
1. When reporting issues, include:
|
||||
- Grafana version
|
||||
- AWS region
|
||||
- Error messages (redact sensitive information)
|
||||
- Steps to reproduce
|
||||
- Query configuration (redact credentials and account IDs)
|
||||
@@ -124,6 +124,8 @@ For more information about dashboard permissions, refer to [Dashboard permission
|
||||
## Restore deleted dashboards
|
||||
|
||||
{{% admonition type="caution" %}}
|
||||
Restoring deleted dashboards is currently in private preview. Grafana Labs offers support on a best-effort basis, and breaking changes might occur prior to the feature being made generally available.
|
||||
|
||||
The feature is only available in Grafana Cloud.
|
||||
{{% /admonition %}}
|
||||
|
||||
|
||||
33
go.mod
33
go.mod
@@ -32,13 +32,14 @@ require (
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // @grafana/plugins-platform-backend
|
||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||
github.com/aws/smithy-go v1.23.1 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
|
||||
@@ -88,14 +89,14 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // @grafana/grafana-backend-group
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff // @grafana/grafana-backend-group
|
||||
github.com/grafana/e2e v0.1.1 // @grafana-app-platform-squad
|
||||
github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 // @grafana/grafana-app-platform-squad
|
||||
@@ -146,10 +147,11 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk v1.16.0 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // @grafana/grafana-backend-group
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // @grafana/identity-access-team
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c // @grafana/identity-access-team
|
||||
github.com/openfga/openfga v1.11.1 // @grafana/identity-access-team
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1 // @grafana/grafana-search-and-storage
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2 // @grafana/grafana-search-and-storage
|
||||
github.com/opentracing/opentracing-go v1.2.0 // @grafana/grafana-search-and-storage
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-backend
|
||||
@@ -344,8 +346,8 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
@@ -470,7 +472,7 @@ require (
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.2 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.3 // indirect
|
||||
github.com/hashicorp/serf v0.10.2 // indirect
|
||||
github.com/hashicorp/vault/api v1.20.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
@@ -515,7 +517,7 @@ require (
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/vsock v1.2.1 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
@@ -543,23 +545,22 @@ require (
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.15.0 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
|
||||
@@ -573,7 +574,7 @@ require (
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0 // indirect
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 // indirect
|
||||
|
||||
32
go.sum
32
go.sum
@@ -850,8 +850,8 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
@@ -862,10 +862,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
@@ -892,14 +892,16 @@ github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:Pwbxovp
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
@@ -1627,14 +1629,20 @@ github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopK
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f h1:OfVtnO3+Ficm7W69dFD5IaZWlMvOLIWBBnppE99dVkU=
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f/go.mod h1:KUNx2Qz7mgh2tm2/TJXx0+uq5SkCrquCFI+dHln2Q50=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20251203163023-dd5a97c606e3/go.mod h1:CZ5McGzO/q6lnRb8xvTODCC2bJniQoQ+gho0AVZC/zY=
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f h1:5ZI6e22sGdg36MAIMJkH6PUHtZU/QuwAScNfgWNlK0I=
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f/go.mod h1:j+YTXmAcD4zCNyl4QSNqYSEe/q9KgrH1btodnhK29hI=
|
||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||
github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg=
|
||||
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
||||
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff/go.mod h1:/pHIcyeZJBZbtboXOjRtPaMl5KK+2VRdNJbCHDkpDYs=
|
||||
github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc=
|
||||
github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE=
|
||||
github.com/grafana/go-mysql-server v0.20.1-grafana1 h1:yA4Mzt+tTdIlQutBUaiPnepULPQ7CS4hMu2GOpHqT6s=
|
||||
@@ -1643,6 +1651,7 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO
|
||||
github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI=
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl6LhqBdbyAQ9YFhExwsj4bjh5vwMNRZY=
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8=
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
@@ -1795,6 +1804,7 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI=
|
||||
github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4=
|
||||
github.com/hashicorp/memberlist v0.5.3/go.mod h1:h60o12SZn/ua/j0B6iKAZezA4eDaGsIuPO70eOaJ6WE=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
@@ -2049,6 +2059,7 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
|
||||
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
@@ -2203,9 +2214,11 @@ github.com/openfga/openfga v1.11.1 h1:+cJBPi/J+RWPRg+cXOjwWEwjauiW8rdE3kEzcFy1ME
|
||||
github.com/openfga/openfga v1.11.1/go.mod h1:MuTGr/ghY7t2sEGwS/59pq9SkqO0QY1kQLIe8Upt+G8=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1 h1:Ws7IN1zyiL1DFqKQPhRXuKe5pLYzMfdxnC1qtajE2PE=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1/go.mod h1:Nu6sz+4zzgxXu8rvKfnwjBEmHsuhTigxRwV2RhELrS8=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2/go.mod h1:glU6rl1Fhfp9aXUHkE36K2mR4ht8vih0ekOVlWKEUHM=
|
||||
github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w=
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w=
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU=
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0/go.mod h1:S0p+X9p6dcBkoMTL+Qq2VOvxKs9ys5PpYWXWqlCS0bQ=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
@@ -2239,6 +2252,7 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -2317,6 +2331,7 @@ github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57J
|
||||
github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
|
||||
github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@@ -2401,6 +2416,7 @@ github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQcc
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0 h1:ScvS2Ga9snVkpOahln/BCLySr3/iBAHJf25u66DweZ0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0/go.mod h1:Dxkqms3OJadP5zirIBPLi9FV8Qpys3T3w40XPEcVsu0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1/go.mod h1:C0tsTuRMONSY+Xf7pv7RMW1/JlewY1+wS8SZE+1lf1s=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
|
||||
21
go.work.sum
21
go.work.sum
@@ -317,6 +317,7 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0 h1:RAHqDHJmNMLe6JvDoRIlXmb72w+62Ue/k5p/qP9yfAg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0/go.mod h1:dtCRwgvytbGKWdlrjMOg9geBoRwRpCYWIOM/JhVsDIc=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig=
|
||||
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=
|
||||
github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns=
|
||||
github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8=
|
||||
@@ -423,6 +424,7 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1 h1:nMp7diZObd4XEVUR0pEvn7/E13JI
|
||||
github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
@@ -436,8 +438,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQG
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs=
|
||||
@@ -491,6 +495,7 @@ github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY=
|
||||
github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
@@ -875,7 +880,6 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gophercloud/gophercloud v1.13.0 h1:8iY9d1DAbzMW6Vok1AxbbK5ZaUjzMp0tdyt4fX9IeJ0=
|
||||
github.com/gophercloud/gophercloud v1.13.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
@@ -886,19 +890,23 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20251203163023-dd5a97c606e3 h1:T4AMrL8ZB1U25m/+FOmkqWPnz0X7u/Oqj1ISg4OrS2c=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
|
||||
github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs=
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff h1:eDbrQsfY1Y3vMfuy5suGI2DRNC1DFBcZMFMlNbPrdiE=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90=
|
||||
github.com/grafana/go-mysql-server v0.20.1-0.20251027172658-317a8d46ffa4/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8=
|
||||
github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b h1:5qp8/5YPt/Z2RW5QHsxvwE05+LWQYIXydP2MwOkMfb8=
|
||||
github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM=
|
||||
github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU=
|
||||
@@ -1020,6 +1028,7 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI
|
||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/memberlist v0.5.3 h1:tQ1jOCypD0WvMemw/ZhhtH+PWpzcftQvgCorLu0hndk=
|
||||
github.com/hashicorp/raft v1.7.0 h1:4u24Qn6lQ6uwziM++UgsyiT64Q8GyRn43CV41qPiz1o=
|
||||
github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
|
||||
github.com/hashicorp/raft-wal v0.4.1 h1:aU8XZ6x8R9BAIB/83Z1dTDtXvDVmv9YVYeXxd/1QBSA=
|
||||
@@ -1180,6 +1189,7 @@ github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxW
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/minio/minio-go/v7 v7.0.75/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
|
||||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
@@ -1339,6 +1349,8 @@ github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369/go.mod h1:m74TNg
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe/go.mod h1:5Z0pbTT7Jz/oQFLfadb+C5t5NwHrduAO7j7L07Ec1GM=
|
||||
github.com/openfga/openfga v1.10.0/go.mod h1:6/m4GTwQsqECsGYQVD3t5sCX97rh3smnmxbMa3YAtJk=
|
||||
github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2 h1:MP16Ozc59kqqwn1v18aQxpeGZhsBanJ2iurZYaQSZ+g=
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0 h1:cZBWc4pA4e65tqTJddbflK435S0tDImj6c9BMvkdUH0=
|
||||
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
@@ -1361,6 +1373,7 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2
|
||||
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
|
||||
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/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
|
||||
@@ -1385,6 +1398,7 @@ github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8N
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
|
||||
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
|
||||
github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=
|
||||
github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8HanlO+2N/Wjv7w=
|
||||
@@ -1446,6 +1460,7 @@ github.com/sercand/kuberesolver v2.4.0+incompatible h1:WE2OlRf6wjLxHwNkkFLQGaZcV
|
||||
github.com/sercand/kuberesolver v2.4.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJPl/ZshwPsX/n4Y7u0CW9E7aQIQ=
|
||||
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
|
||||
github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1 h1:XZUTA0gy/lgDYp/UhEwv7Js24F1j8NJ833QrWv0Xux4=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
@@ -1942,7 +1957,9 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
@@ -1955,6 +1972,7 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
@@ -2126,6 +2144,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
|
||||
|
||||
@@ -58,14 +58,12 @@
|
||||
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"clean": "rimraf ./dist ./compiled ./unstable ./testing ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"i18n-extract": "i18next-cli extract --sync-primary"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/test-utils": "workspace:*",
|
||||
"@rtk-query/codegen-openapi": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -96,6 +94,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.13.5",
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"@grafana/api-clients": "12.4.0-pre",
|
||||
"@grafana/i18n": "12.4.0-pre",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"fishery": "^2.3.1",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Re-generate the clients
|
||||
|
||||
⚠️ This guide assumes the Backend definitions have been updated in `apps/alerting`.
|
||||
|
||||
## Re-create OpenAPI specification
|
||||
|
||||
Start with re-generating the OpenAPI snapshots by running the test in `pkg/tests/apis/openapi_test.go`.
|
||||
|
||||
This will output the OpenAPI JSON spec file(s) in `pkg/tests/apis/openapi_snapshots`.
|
||||
|
||||
## Process OpenAPI specifications
|
||||
|
||||
Next up run the post-processing of the snapshots with `yarn run process-specs`, this will copy processed specifications to `./data/openapi/`.
|
||||
|
||||
## Generate RTKQ files
|
||||
|
||||
These files are built using the `yarn run codegen` command, make sure to run that in the Grafana Alerting package working directory.
|
||||
|
||||
`yarn --cwd ./packages/grafana-alerting run codegen`.
|
||||
|
||||
API clients will be written to `src/grafana/api/<version>/api.gen.ts`.
|
||||
|
||||
Make sure to create a versioned API client for each API version – see `src/grafana/api/v0alpha1/api.ts` as an example.
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* This script will generate TypeScript type definitions and a RTKQ clients for the alerting k8s APIs.
|
||||
*
|
||||
* Run `yarn run codegen` from the "grafana-alerting" package to invoke this script.
|
||||
*
|
||||
* API clients will be placed in "src/grafana/api/<version>/api.gen.ts"
|
||||
*/
|
||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
// ℹ️ append API groups and versions here to generate additional API clients
|
||||
const SPECS = [
|
||||
['notifications.alerting.grafana.app', ['v0alpha1']],
|
||||
['rules.alerting.grafana.app', ['v0alpha1']],
|
||||
// keep this in Grafana Enterprise
|
||||
// ['alertenrichment.grafana.app', ['v1beta1']],
|
||||
] as const;
|
||||
|
||||
type OutputFile = Omit<ConfigFile, 'outputFile'>;
|
||||
type OutputFiles = Record<string, OutputFile>;
|
||||
|
||||
const outputFiles = SPECS.reduce<OutputFiles>((groupAcc, [group, versions]) => {
|
||||
return versions.reduce<OutputFiles>((versionAcc, version) => {
|
||||
// Create a unique export name based on the group
|
||||
const groupName = group.split('.')[0]; // e.g., 'notifications', 'rules', 'alertenrichment'
|
||||
const exportName = `${groupName}API`;
|
||||
|
||||
// ℹ️ these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go" and "scripts/process-specs.ts",
|
||||
// see the README in the "openapi_snapshots" directory
|
||||
const schemaFile = `../../../data/openapi/${group}-${version}.json`;
|
||||
|
||||
// ℹ️ make sure there is a API file in each versioned directory
|
||||
const apiFile = `../src/grafana/api/${groupName}/${version}/api.ts`;
|
||||
|
||||
// output each api client into a versioned directory with group-specific naming
|
||||
const outputPath = `../src/grafana/api/${groupName}/${version}/${groupName}.api.gen.ts`;
|
||||
|
||||
versionAcc[outputPath] = {
|
||||
exportName,
|
||||
schemaFile,
|
||||
apiFile,
|
||||
tag: true, // generate tags for cache invalidation
|
||||
} satisfies OutputFile;
|
||||
|
||||
return versionAcc;
|
||||
}, groupAcc);
|
||||
}, {});
|
||||
|
||||
export default {
|
||||
// these are intentionally empty but will be set for each versioned config file
|
||||
exportName: '',
|
||||
schemaFile: '',
|
||||
apiFile: '',
|
||||
|
||||
outputFiles,
|
||||
} satisfies ConfigFile;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL, getAPIReducerPath } from '../../util';
|
||||
|
||||
import { GROUP, VERSION } from './const';
|
||||
|
||||
const baseUrl = getAPIBaseURL(GROUP, VERSION);
|
||||
const reducerPath = getAPIReducerPath(GROUP, VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath,
|
||||
baseQuery: fetchBaseQuery({
|
||||
// Set URL correctly so MSW can intercept requests
|
||||
// https://mswjs.io/docs/runbook#rtk-query-requests-are-not-intercepted
|
||||
baseUrl: new URL(baseUrl, globalThis.location.origin).href,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export const VERSION = 'v0alpha1' as const;
|
||||
export const GROUP = 'notifications.alerting.grafana.app' as const;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Factory } from 'fishery';
|
||||
|
||||
import { API_GROUP, API_VERSION } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { DEFAULT_NAMESPACE, generateResourceVersion, generateTitle, generateUID } from '../../../../../mocks/util';
|
||||
import { GROUP, VERSION } from '../../const';
|
||||
import {
|
||||
ContactPoint,
|
||||
ContactPointMetadataAnnotations,
|
||||
@@ -14,7 +15,7 @@ import { AlertingEntityMetadataAnnotationsFactory } from './common';
|
||||
|
||||
export const ListReceiverApiResponseFactory = Factory.define<EnhancedListReceiverApiResponse>(() => ({
|
||||
kind: 'ReceiverList',
|
||||
apiVersion: `${GROUP}/${VERSION}`,
|
||||
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
||||
metadata: {
|
||||
resourceVersion: generateResourceVersion(),
|
||||
},
|
||||
@@ -26,7 +27,7 @@ export const ContactPointFactory = Factory.define<ContactPoint>(() => {
|
||||
|
||||
return {
|
||||
kind: 'Receiver',
|
||||
apiVersion: `${GROUP}/${VERSION}`,
|
||||
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
||||
metadata: {
|
||||
name: btoa(title),
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
CreateReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { CreateReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function createReceiverHandler(
|
||||
data: CreateReceiverApiResponse | ((info: Parameters<Parameters<typeof http.post>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.post(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||
return http.post(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
DeleteReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { DeleteReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function deleteReceiverHandler(
|
||||
data: DeleteReceiverApiResponse | ((info: Parameters<Parameters<typeof http.delete>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.delete(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||
return http.delete(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers/:name'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
DeletecollectionReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { DeletecollectionReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function deletecollectionReceiverHandler(
|
||||
data: DeletecollectionReceiverApiResponse | ((info: Parameters<Parameters<typeof http.delete>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.delete(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||
return http.delete(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
GetReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { GetReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function getReceiverHandler(
|
||||
data: GetReceiverApiResponse | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.get(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||
return http.get(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers/:name'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import { API_GROUP, API_VERSION } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
import { EnhancedListReceiverApiResponse } from '../../../types';
|
||||
|
||||
export function listReceiverHandler(
|
||||
data: EnhancedListReceiverApiResponse | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.get(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers'), function handler(info) {
|
||||
return http.get(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
ReplaceReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { ReplaceReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function replaceReceiverHandler(
|
||||
data: ReplaceReceiverApiResponse | ((info: Parameters<Parameters<typeof http.put>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.put(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||
return http.put(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers/:name'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
API_GROUP,
|
||||
API_VERSION,
|
||||
UpdateReceiverApiResponse,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { getAPIBaseURLForMocks } from '../../../../../../mocks/util';
|
||||
import { UpdateReceiverApiResponse } from '../../../../v0alpha1/notifications.api.gen';
|
||||
import { GROUP, VERSION } from '../../../const';
|
||||
|
||||
export function updateReceiverHandler(
|
||||
data: UpdateReceiverApiResponse | ((info: Parameters<Parameters<typeof http.patch>[1]>[0]) => Response)
|
||||
) {
|
||||
return http.patch(getAPIBaseURLForMocks(GROUP, VERSION, '/receivers/:name'), function handler(info) {
|
||||
return http.patch(getAPIBaseURLForMocks(API_GROUP, API_VERSION, '/receivers/:name'), function handler(info) {
|
||||
if (typeof data === 'function') {
|
||||
return data(info);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
*/
|
||||
import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest';
|
||||
|
||||
import type { ListReceiverApiResponse, Receiver, ReceiverIntegration } from './notifications.api.gen';
|
||||
import type {
|
||||
ListReceiverApiResponse,
|
||||
Receiver,
|
||||
ReceiverIntegration,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
type GenericIntegration = OverrideProperties<
|
||||
ReceiverIntegration,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL, getAPIReducerPath } from '../../util';
|
||||
|
||||
import { GROUP, VERSION } from './const';
|
||||
|
||||
const baseUrl = getAPIBaseURL(GROUP, VERSION);
|
||||
const reducerPath = getAPIReducerPath(GROUP, VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath,
|
||||
baseQuery: fetchBaseQuery({
|
||||
// Set URL correctly so MSW can intercept requests
|
||||
// https://mswjs.io/docs/runbook#rtk-query-requests-are-not-intercepted
|
||||
baseUrl: new URL(baseUrl, globalThis.location.origin).href,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export const VERSION = 'v0alpha1' as const;
|
||||
export const GROUP = 'rules.alerting.grafana.app' as const;
|
||||
@@ -7,9 +7,10 @@ import { OverrideProperties } from 'type-fest';
|
||||
|
||||
import {
|
||||
CreateReceiverApiArg,
|
||||
type ListReceiverApiArg,
|
||||
notificationsAPI,
|
||||
} from '../../../api/notifications/v0alpha1/notifications.api.gen';
|
||||
ListReceiverApiArg,
|
||||
generatedAPI as notificationsAPIv0alpha1,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import type { ContactPoint, EnhancedListReceiverApiResponse } from '../../../api/notifications/v0alpha1/types';
|
||||
|
||||
// this is a workaround for the fact that the generated types are not narrow enough
|
||||
@@ -22,17 +23,17 @@ type ListContactPointsHookResult = TypedUseQueryHookResult<
|
||||
// Type for the options that can be passed to the hook
|
||||
// Based on the pattern used for mutation options in this file
|
||||
type ListContactPointsQueryArgs = Parameters<
|
||||
typeof notificationsAPI.endpoints.listReceiver.useQuery<ListContactPointsHookResult>
|
||||
typeof notificationsAPIv0alpha1.endpoints.listReceiver.useQuery<ListContactPointsHookResult>
|
||||
>[0];
|
||||
|
||||
type ListContactPointsQueryOptions = Parameters<
|
||||
typeof notificationsAPI.endpoints.listReceiver.useQuery<ListContactPointsHookResult>
|
||||
typeof notificationsAPIv0alpha1.endpoints.listReceiver.useQuery<ListContactPointsHookResult>
|
||||
>[1];
|
||||
|
||||
/**
|
||||
* useListContactPoints is a hook that fetches a list of contact points
|
||||
*
|
||||
* This function wraps the notificationsAPI.useListReceiverQuery with proper typing
|
||||
* This function wraps the notificationsAPIv0alpha1.useListReceiverQuery with proper typing
|
||||
* to ensure that the returned ContactPoints are correctly typed in the data.items array.
|
||||
*
|
||||
* It automatically uses the configured namespace for the query.
|
||||
@@ -43,8 +44,8 @@ type ListContactPointsQueryOptions = Parameters<
|
||||
export function useListContactPoints(
|
||||
queryArgs: ListContactPointsQueryArgs = {},
|
||||
queryOptions: ListContactPointsQueryOptions = {}
|
||||
) {
|
||||
return notificationsAPI.useListReceiverQuery<ListContactPointsHookResult>(queryArgs, queryOptions);
|
||||
): ListContactPointsHookResult {
|
||||
return notificationsAPIv0alpha1.useListReceiverQuery<ListContactPointsHookResult>(queryArgs, queryOptions);
|
||||
}
|
||||
|
||||
// type narrowing mutations requires us to define a few helper types
|
||||
@@ -60,7 +61,7 @@ type CreateContactPointMutation = TypedUseMutationResult<
|
||||
>;
|
||||
|
||||
type UseCreateContactPointOptions = Parameters<
|
||||
typeof notificationsAPI.endpoints.createReceiver.useMutation<CreateContactPointMutation>
|
||||
typeof notificationsAPIv0alpha1.endpoints.createReceiver.useMutation<CreateContactPointMutation>
|
||||
>[0];
|
||||
|
||||
/**
|
||||
@@ -69,8 +70,16 @@ type UseCreateContactPointOptions = Parameters<
|
||||
* This function wraps the notificationsAPI.useCreateReceiverMutation with proper typing
|
||||
* to ensure that the payload supports type narrowing.
|
||||
*/
|
||||
export function useCreateContactPoint(options?: UseCreateContactPointOptions) {
|
||||
const [updateFn, result] = notificationsAPI.endpoints.createReceiver.useMutation<CreateContactPointMutation>(options);
|
||||
export function useCreateContactPoint(
|
||||
options?: UseCreateContactPointOptions
|
||||
): readonly [
|
||||
(
|
||||
args: CreateContactPointArgs
|
||||
) => ReturnType<ReturnType<typeof notificationsAPIv0alpha1.endpoints.createReceiver.useMutation>[0]>,
|
||||
ReturnType<typeof notificationsAPIv0alpha1.endpoints.createReceiver.useMutation<CreateContactPointMutation>>[1],
|
||||
] {
|
||||
const [updateFn, result] =
|
||||
notificationsAPIv0alpha1.endpoints.createReceiver.useMutation<CreateContactPointMutation>(options);
|
||||
|
||||
const typedUpdateFn = (args: CreateContactPointArgs) => {
|
||||
// @ts-expect-error this one is just impossible for me to figure out
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { countBy, isEmpty } from 'lodash';
|
||||
|
||||
import { Receiver } from '../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import { Receiver } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { ContactPoint } from '../api/notifications/v0alpha1/types';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RoutingTreeMatcher } from '../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import { RoutingTreeMatcher } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
export type Label = [string, string];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VERSION } from '../../api/notifications/v0alpha1/const';
|
||||
import { API_VERSION, RoutingTree } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { LabelMatcherFactory, RouteFactory } from '../../api/notifications/v0alpha1/mocks/fakes/Routes';
|
||||
import { RoutingTree } from '../../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import { Label } from '../../matchers/types';
|
||||
|
||||
import { matchInstancesToRouteTrees } from './useMatchPolicies';
|
||||
@@ -16,7 +16,7 @@ describe('matchInstancesToRouteTrees', () => {
|
||||
const trees: RoutingTree[] = [
|
||||
{
|
||||
kind: 'RoutingTree',
|
||||
apiVersion: VERSION,
|
||||
apiVersion: API_VERSION,
|
||||
metadata: { name: treeName },
|
||||
spec: {
|
||||
defaults: {
|
||||
@@ -24,7 +24,6 @@ describe('matchInstancesToRouteTrees', () => {
|
||||
},
|
||||
routes: [route],
|
||||
},
|
||||
status: {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -51,7 +50,7 @@ describe('matchInstancesToRouteTrees', () => {
|
||||
const trees: RoutingTree[] = [
|
||||
{
|
||||
kind: 'RoutingTree',
|
||||
apiVersion: VERSION,
|
||||
apiVersion: API_VERSION,
|
||||
metadata: { name: treeName },
|
||||
spec: {
|
||||
defaults: {
|
||||
@@ -59,7 +58,6 @@ describe('matchInstancesToRouteTrees', () => {
|
||||
},
|
||||
routes: [route],
|
||||
},
|
||||
status: {},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RoutingTree, notificationsAPI } from '../../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import {
|
||||
RoutingTree,
|
||||
generatedAPI as notificationsAPIv0alpha1,
|
||||
} from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { Label } from '../../matchers/types';
|
||||
import { USER_DEFINED_TREE_NAME } from '../consts';
|
||||
import { Route, RouteWithID } from '../types';
|
||||
@@ -24,6 +28,11 @@ export type InstanceMatchResult = {
|
||||
matchedRoutes: RouteMatch[];
|
||||
};
|
||||
|
||||
interface UseMatchInstancesToRouteTreesReturnType
|
||||
extends ReturnType<typeof notificationsAPIv0alpha1.endpoints.listRoutingTree.useQuery> {
|
||||
matchInstancesToRouteTrees: (instances: Label[][]) => InstanceMatchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that finds notification policy routes in all routing trees that match the provided set of alert instances.
|
||||
*
|
||||
@@ -35,8 +44,8 @@ export type InstanceMatchResult = {
|
||||
* @returns An object containing a `matchInstancesToRoutingTrees` function that takes alert instances
|
||||
* and returns an array of InstanceMatchResult objects, each containing the matched routes and matching details
|
||||
*/
|
||||
export function useMatchInstancesToRouteTrees() {
|
||||
const { data, ...rest } = notificationsAPI.endpoints.listRoutingTree.useQuery(
|
||||
export function useMatchInstancesToRouteTrees(): UseMatchInstancesToRouteTreesReturnType {
|
||||
const { data, ...rest } = notificationsAPIv0alpha1.endpoints.listRoutingTree.useQuery(
|
||||
{},
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OverrideProperties } from 'type-fest';
|
||||
|
||||
import { RoutingTreeRoute } from '../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import { RoutingTreeRoute } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { LabelMatcher } from '../matchers/types';
|
||||
|
||||
// type-narrow the route tree
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { groupBy, isArray, pick, reduce, uniqueId } from 'lodash';
|
||||
|
||||
import { RoutingTree, RoutingTreeRoute } from '../api/notifications/v0alpha1/notifications.api.gen';
|
||||
import { RoutingTree, RoutingTreeRoute } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
|
||||
import { Label } from '../matchers/types';
|
||||
import { LabelMatchDetails, matchLabels } from '../matchers/utils';
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@ export { type LabelMatcher, type Label } from './grafana/matchers/types';
|
||||
export { matchLabelsSet, matchLabels, isLabelMatch, type LabelMatchDetails } from './grafana/matchers/utils';
|
||||
|
||||
// API endpoints
|
||||
export { notificationsAPI as notificationsAPIv0alpha1 } from './grafana/api/notifications/v0alpha1/notifications.api.gen';
|
||||
export { rulesAPI as rulesAPIv0alpha1 } from './grafana/api/rules/v0alpha1/rules.api.gen';
|
||||
export { generatedAPI as notificationsAPIv0alpha1 } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
export { generatedAPI as rulesAPIv0alpha1 } from '@grafana/api-clients/rtkq/rules.alerting/v0alpha1';
|
||||
|
||||
@@ -2,13 +2,25 @@ import { configureStore } from '@reduxjs/toolkit';
|
||||
import { useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { notificationsAPIv0alpha1 } from '../src/unstable';
|
||||
import { MockBackendSrv } from '@grafana/api-clients';
|
||||
import { generatedAPI as notificationsAPIv0alpha1 } from '@grafana/api-clients/rtkq/notifications.alerting/v0alpha1';
|
||||
import { generatedAPI as rulesAPIv0alpha1 } from '@grafana/api-clients/rtkq/rules.alerting/v0alpha1';
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
|
||||
// Initialize BackendSrv for tests - this allows RTKQ to make HTTP requests
|
||||
// The actual HTTP requests will be intercepted by MSW (setupMockServer)
|
||||
// We only need to implement fetch() which is what RTKQ uses
|
||||
// we could remove this once @grafana/api-client no longer uses the BackendSrv
|
||||
// @ts-ignore
|
||||
setBackendSrv(new MockBackendSrv());
|
||||
|
||||
// create an empty store
|
||||
export const store = configureStore({
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(notificationsAPIv0alpha1.middleware),
|
||||
export const store: ReturnType<typeof configureStore> = configureStore({
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(notificationsAPIv0alpha1.middleware).concat(rulesAPIv0alpha1.middleware),
|
||||
reducer: {
|
||||
[notificationsAPIv0alpha1.reducerPath]: notificationsAPIv0alpha1.reducer,
|
||||
[rulesAPIv0alpha1.reducerPath]: rulesAPIv0alpha1.reducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,14 @@ import '@testing-library/jest-dom';
|
||||
* method which wraps the passed element in all of the necessary Providers,
|
||||
* so it can render correctly in the context of the application
|
||||
*/
|
||||
const customRender = (ui: React.ReactNode, renderOptions: RenderOptions = {}) => {
|
||||
const customRender = (
|
||||
ui: React.ReactNode,
|
||||
renderOptions: RenderOptions = {}
|
||||
): {
|
||||
renderResult: ReturnType<typeof render>;
|
||||
user: ReturnType<typeof userEvent.setup>;
|
||||
store: typeof store;
|
||||
} => {
|
||||
const user = userEvent.setup();
|
||||
const Providers = renderOptions.wrapper || getDefaultWrapper();
|
||||
|
||||
|
||||
@@ -116,6 +116,18 @@
|
||||
"import": "./dist/esm/clients/rtkq/shorturl/v1beta1/index.mjs",
|
||||
"require": "./dist/cjs/clients/rtkq/shorturl/v1beta1/index.cjs"
|
||||
},
|
||||
"./rtkq/notifications.alerting/v0alpha1": {
|
||||
"@grafana-app/source": "./src/clients/rtkq/notifications.alerting/v0alpha1/index.ts",
|
||||
"types": "./dist/types/clients/rtkq/notifications.alerting/v0alpha1/index.d.ts",
|
||||
"import": "./dist/esm/clients/rtkq/notifications.alerting/v0alpha1/index.mjs",
|
||||
"require": "./dist/cjs/clients/rtkq/notifications.alerting/v0alpha1/index.cjs"
|
||||
},
|
||||
"./rtkq/rules.alerting/v0alpha1": {
|
||||
"@grafana-app/source": "./src/clients/rtkq/rules.alerting/v0alpha1/index.ts",
|
||||
"types": "./dist/types/clients/rtkq/rules.alerting/v0alpha1/index.d.ts",
|
||||
"import": "./dist/esm/clients/rtkq/rules.alerting/v0alpha1/index.mjs",
|
||||
"require": "./dist/cjs/clients/rtkq/rules.alerting/v0alpha1/index.cjs"
|
||||
},
|
||||
"./rtkq/historian.alerting/v0alpha1": {
|
||||
"@grafana-app/source": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts",
|
||||
"types": "./dist/types/clients/rtkq/historian.alerting/v0alpha1/index.d.ts",
|
||||
|
||||
@@ -10,10 +10,12 @@ import { generatedAPI as historianAlertingAPIv0alpha1 } from './historian.alerti
|
||||
import { generatedAPI as iamAPIv0alpha1 } from './iam/v0alpha1';
|
||||
import { generatedAPI as logsdrilldownAPIv1alpha1 } from './logsdrilldown/v1alpha1';
|
||||
import { generatedAPI as migrateToCloudAPI } from './migrate-to-cloud';
|
||||
import { generatedAPI as notificationsAlertingAPIv0alpha1 } from './notifications.alerting/v0alpha1';
|
||||
import { generatedAPI as playlistAPIv0alpha1 } from './playlist/v0alpha1';
|
||||
import { generatedAPI as preferencesUserAPI } from './preferences/user';
|
||||
import { generatedAPI as preferencesAPIv1alpha1 } from './preferences/v1alpha1';
|
||||
import { generatedAPI as provisioningAPIv0alpha1 } from './provisioning/v0alpha1';
|
||||
import { generatedAPI as rulesAlertingAPIv0alpha1 } from './rules.alerting/v0alpha1';
|
||||
import { generatedAPI as shortURLAPIv1beta1 } from './shorturl/v1beta1';
|
||||
import { generatedAPI as legacyUserAPI } from './user';
|
||||
// PLOP_INJECT_IMPORT
|
||||
@@ -33,6 +35,8 @@ export const allMiddleware = [
|
||||
shortURLAPIv1beta1.middleware,
|
||||
correlationsAPIv0alpha1.middleware,
|
||||
legacyUserAPI.middleware,
|
||||
notificationsAlertingAPIv0alpha1.middleware,
|
||||
rulesAlertingAPIv0alpha1.middleware,
|
||||
historianAlertingAPIv0alpha1.middleware,
|
||||
logsdrilldownAPIv1alpha1.middleware,
|
||||
// PLOP_INJECT_MIDDLEWARE
|
||||
@@ -53,6 +57,8 @@ export const allReducers = {
|
||||
[shortURLAPIv1beta1.reducerPath]: shortURLAPIv1beta1.reducer,
|
||||
[correlationsAPIv0alpha1.reducerPath]: correlationsAPIv0alpha1.reducer,
|
||||
[legacyUserAPI.reducerPath]: legacyUserAPI.reducer,
|
||||
[notificationsAlertingAPIv0alpha1.reducerPath]: notificationsAlertingAPIv0alpha1.reducer,
|
||||
[rulesAlertingAPIv0alpha1.reducerPath]: rulesAlertingAPIv0alpha1.reducer,
|
||||
[historianAlertingAPIv0alpha1.reducerPath]: historianAlertingAPIv0alpha1.reducer,
|
||||
[logsdrilldownAPIv1alpha1.reducerPath]: logsdrilldownAPIv1alpha1.reducer,
|
||||
// PLOP_INJECT_REDUCER
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'notifications.alerting.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'notificationsAlertingAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from './api';
|
||||
import { api } from './baseAPI';
|
||||
export const addTagTypes = ['API Discovery', 'Receiver', 'RoutingTree', 'TemplateGroup', 'TimeInterval'] as const;
|
||||
const injectedRtkApi = api
|
||||
.enhanceEndpoints({
|
||||
@@ -7,7 +7,7 @@ const injectedRtkApi = api
|
||||
.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getApiResources: build.query<GetApiResourcesApiResponse, GetApiResourcesApiArg>({
|
||||
query: () => ({ url: `/apis/notifications.alerting.grafana.app/v0alpha1/` }),
|
||||
query: () => ({ url: `/` }),
|
||||
providesTags: ['API Discovery'],
|
||||
}),
|
||||
listReceiver: build.query<ListReceiverApiResponse, ListReceiverApiArg>({
|
||||
@@ -119,44 +119,6 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['Receiver'],
|
||||
}),
|
||||
getReceiverStatus: build.query<GetReceiverStatusApiResponse, GetReceiverStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/receivers/${queryArg.name}/status`,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
},
|
||||
}),
|
||||
providesTags: ['Receiver'],
|
||||
}),
|
||||
replaceReceiverStatus: build.mutation<ReplaceReceiverStatusApiResponse, ReplaceReceiverStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/receivers/${queryArg.name}/status`,
|
||||
method: 'PUT',
|
||||
body: queryArg.receiver,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['Receiver'],
|
||||
}),
|
||||
updateReceiverStatus: build.mutation<UpdateReceiverStatusApiResponse, UpdateReceiverStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/receivers/${queryArg.name}/status`,
|
||||
method: 'PATCH',
|
||||
body: queryArg.patch,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['Receiver'],
|
||||
}),
|
||||
listRoutingTree: build.query<ListRoutingTreeApiResponse, ListRoutingTreeApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/routingtrees`,
|
||||
@@ -269,44 +231,6 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['RoutingTree'],
|
||||
}),
|
||||
getRoutingTreeStatus: build.query<GetRoutingTreeStatusApiResponse, GetRoutingTreeStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/routingtrees/${queryArg.name}/status`,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
},
|
||||
}),
|
||||
providesTags: ['RoutingTree'],
|
||||
}),
|
||||
replaceRoutingTreeStatus: build.mutation<ReplaceRoutingTreeStatusApiResponse, ReplaceRoutingTreeStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/routingtrees/${queryArg.name}/status`,
|
||||
method: 'PUT',
|
||||
body: queryArg.routingTree,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['RoutingTree'],
|
||||
}),
|
||||
updateRoutingTreeStatus: build.mutation<UpdateRoutingTreeStatusApiResponse, UpdateRoutingTreeStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/routingtrees/${queryArg.name}/status`,
|
||||
method: 'PATCH',
|
||||
body: queryArg.patch,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['RoutingTree'],
|
||||
}),
|
||||
listTemplateGroup: build.query<ListTemplateGroupApiResponse, ListTemplateGroupApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/templategroups`,
|
||||
@@ -419,47 +343,6 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['TemplateGroup'],
|
||||
}),
|
||||
getTemplateGroupStatus: build.query<GetTemplateGroupStatusApiResponse, GetTemplateGroupStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/templategroups/${queryArg.name}/status`,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
},
|
||||
}),
|
||||
providesTags: ['TemplateGroup'],
|
||||
}),
|
||||
replaceTemplateGroupStatus: build.mutation<
|
||||
ReplaceTemplateGroupStatusApiResponse,
|
||||
ReplaceTemplateGroupStatusApiArg
|
||||
>({
|
||||
query: (queryArg) => ({
|
||||
url: `/templategroups/${queryArg.name}/status`,
|
||||
method: 'PUT',
|
||||
body: queryArg.templateGroup,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['TemplateGroup'],
|
||||
}),
|
||||
updateTemplateGroupStatus: build.mutation<UpdateTemplateGroupStatusApiResponse, UpdateTemplateGroupStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/templategroups/${queryArg.name}/status`,
|
||||
method: 'PATCH',
|
||||
body: queryArg.patch,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['TemplateGroup'],
|
||||
}),
|
||||
listTimeInterval: build.query<ListTimeIntervalApiResponse, ListTimeIntervalApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/timeintervals`,
|
||||
@@ -572,48 +455,10 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['TimeInterval'],
|
||||
}),
|
||||
getTimeIntervalStatus: build.query<GetTimeIntervalStatusApiResponse, GetTimeIntervalStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/timeintervals/${queryArg.name}/status`,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
},
|
||||
}),
|
||||
providesTags: ['TimeInterval'],
|
||||
}),
|
||||
replaceTimeIntervalStatus: build.mutation<ReplaceTimeIntervalStatusApiResponse, ReplaceTimeIntervalStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/timeintervals/${queryArg.name}/status`,
|
||||
method: 'PUT',
|
||||
body: queryArg.timeInterval,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['TimeInterval'],
|
||||
}),
|
||||
updateTimeIntervalStatus: build.mutation<UpdateTimeIntervalStatusApiResponse, UpdateTimeIntervalStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/timeintervals/${queryArg.name}/status`,
|
||||
method: 'PATCH',
|
||||
body: queryArg.patch,
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: ['TimeInterval'],
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as notificationsAPI };
|
||||
export { injectedRtkApi as generatedAPI };
|
||||
export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList;
|
||||
export type GetApiResourcesApiArg = void;
|
||||
export type ListReceiverApiResponse = /** status 200 OK */ ReceiverList;
|
||||
@@ -781,43 +626,6 @@ export type UpdateReceiverApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetReceiverStatusApiResponse = /** status 200 OK */ Receiver;
|
||||
export type GetReceiverStatusApiArg = {
|
||||
/** name of the Receiver */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
};
|
||||
export type ReplaceReceiverStatusApiResponse = /** status 200 OK */ Receiver | /** status 201 Created */ Receiver;
|
||||
export type ReplaceReceiverStatusApiArg = {
|
||||
/** name of the Receiver */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
receiver: Receiver;
|
||||
};
|
||||
export type UpdateReceiverStatusApiResponse = /** status 200 OK */ Receiver | /** status 201 Created */ Receiver;
|
||||
export type UpdateReceiverStatusApiArg = {
|
||||
/** name of the Receiver */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
/** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type ListRoutingTreeApiResponse = /** status 200 OK */ RoutingTreeList;
|
||||
export type ListRoutingTreeApiArg = {
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
@@ -983,47 +791,6 @@ export type UpdateRoutingTreeApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetRoutingTreeStatusApiResponse = /** status 200 OK */ RoutingTree;
|
||||
export type GetRoutingTreeStatusApiArg = {
|
||||
/** name of the RoutingTree */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
};
|
||||
export type ReplaceRoutingTreeStatusApiResponse = /** status 200 OK */
|
||||
| RoutingTree
|
||||
| /** status 201 Created */ RoutingTree;
|
||||
export type ReplaceRoutingTreeStatusApiArg = {
|
||||
/** name of the RoutingTree */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
routingTree: RoutingTree;
|
||||
};
|
||||
export type UpdateRoutingTreeStatusApiResponse = /** status 200 OK */
|
||||
| RoutingTree
|
||||
| /** status 201 Created */ RoutingTree;
|
||||
export type UpdateRoutingTreeStatusApiArg = {
|
||||
/** name of the RoutingTree */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
/** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type ListTemplateGroupApiResponse = /** status 200 OK */ TemplateGroupList;
|
||||
export type ListTemplateGroupApiArg = {
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
@@ -1193,47 +960,6 @@ export type UpdateTemplateGroupApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetTemplateGroupStatusApiResponse = /** status 200 OK */ TemplateGroup;
|
||||
export type GetTemplateGroupStatusApiArg = {
|
||||
/** name of the TemplateGroup */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
};
|
||||
export type ReplaceTemplateGroupStatusApiResponse = /** status 200 OK */
|
||||
| TemplateGroup
|
||||
| /** status 201 Created */ TemplateGroup;
|
||||
export type ReplaceTemplateGroupStatusApiArg = {
|
||||
/** name of the TemplateGroup */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
templateGroup: TemplateGroup;
|
||||
};
|
||||
export type UpdateTemplateGroupStatusApiResponse = /** status 200 OK */
|
||||
| TemplateGroup
|
||||
| /** status 201 Created */ TemplateGroup;
|
||||
export type UpdateTemplateGroupStatusApiArg = {
|
||||
/** name of the TemplateGroup */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
/** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type ListTimeIntervalApiResponse = /** status 200 OK */ TimeIntervalList;
|
||||
export type ListTimeIntervalApiArg = {
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
@@ -1399,47 +1125,6 @@ export type UpdateTimeIntervalApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetTimeIntervalStatusApiResponse = /** status 200 OK */ TimeInterval;
|
||||
export type GetTimeIntervalStatusApiArg = {
|
||||
/** name of the TimeInterval */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
};
|
||||
export type ReplaceTimeIntervalStatusApiResponse = /** status 200 OK */
|
||||
| TimeInterval
|
||||
| /** status 201 Created */ TimeInterval;
|
||||
export type ReplaceTimeIntervalStatusApiArg = {
|
||||
/** name of the TimeInterval */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
timeInterval: TimeInterval;
|
||||
};
|
||||
export type UpdateTimeIntervalStatusApiResponse = /** status 200 OK */
|
||||
| TimeInterval
|
||||
| /** status 201 Created */ TimeInterval;
|
||||
export type UpdateTimeIntervalStatusApiArg = {
|
||||
/** name of the TimeInterval */
|
||||
name: string;
|
||||
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
|
||||
pretty?: string;
|
||||
/** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
|
||||
dryRun?: string;
|
||||
/** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */
|
||||
fieldManager?: string;
|
||||
/** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
|
||||
fieldValidation?: string;
|
||||
/** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type ApiResource = {
|
||||
/** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */
|
||||
categories?: string[];
|
||||
@@ -1572,34 +1257,6 @@ export type ReceiverSpec = {
|
||||
integrations: ReceiverIntegration[];
|
||||
title: string;
|
||||
};
|
||||
export type ReceiverOperatorState = {
|
||||
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||
descriptiveState?: string;
|
||||
/** details contains any extra information that is operator-specific */
|
||||
details?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||
lastEvaluation: string;
|
||||
/** state describes the state of the lastEvaluation.
|
||||
It is limited to three possible states for machine evaluation. */
|
||||
state: 'success' | 'in_progress' | 'failed';
|
||||
};
|
||||
export type ReceiverStatus = {
|
||||
/** additionalFields is reserved for future use */
|
||||
additionalFields?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** operatorStates is a map of operator ID to operator state evaluations.
|
||||
Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||
operatorStates?: {
|
||||
[key: string]: ReceiverOperatorState;
|
||||
};
|
||||
};
|
||||
export type Receiver = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion: string;
|
||||
@@ -1607,7 +1264,6 @@ export type Receiver = {
|
||||
kind: string;
|
||||
metadata: ObjectMeta;
|
||||
spec: ReceiverSpec;
|
||||
status?: ReceiverStatus;
|
||||
};
|
||||
export type ListMeta = {
|
||||
/** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */
|
||||
@@ -1700,34 +1356,6 @@ export type RoutingTreeSpec = {
|
||||
defaults: RoutingTreeRouteDefaults;
|
||||
routes: RoutingTreeRoute[];
|
||||
};
|
||||
export type RoutingTreeOperatorState = {
|
||||
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||
descriptiveState?: string;
|
||||
/** details contains any extra information that is operator-specific */
|
||||
details?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||
lastEvaluation: string;
|
||||
/** state describes the state of the lastEvaluation.
|
||||
It is limited to three possible states for machine evaluation. */
|
||||
state: 'success' | 'in_progress' | 'failed';
|
||||
};
|
||||
export type RoutingTreeStatus = {
|
||||
/** additionalFields is reserved for future use */
|
||||
additionalFields?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** operatorStates is a map of operator ID to operator state evaluations.
|
||||
Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||
operatorStates?: {
|
||||
[key: string]: RoutingTreeOperatorState;
|
||||
};
|
||||
};
|
||||
export type RoutingTree = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion: string;
|
||||
@@ -1735,7 +1363,6 @@ export type RoutingTree = {
|
||||
kind: string;
|
||||
metadata: ObjectMeta;
|
||||
spec: RoutingTreeSpec;
|
||||
status?: RoutingTreeStatus;
|
||||
};
|
||||
export type RoutingTreeList = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
@@ -1745,38 +1372,12 @@ export type RoutingTreeList = {
|
||||
kind?: string;
|
||||
metadata: ListMeta;
|
||||
};
|
||||
export type TemplateGroupTemplateKind = 'grafana' | 'mimir';
|
||||
export type TemplateGroupSpec = {
|
||||
content: string;
|
||||
kind: TemplateGroupTemplateKind;
|
||||
title: string;
|
||||
};
|
||||
export type TemplateGroupOperatorState = {
|
||||
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||
descriptiveState?: string;
|
||||
/** details contains any extra information that is operator-specific */
|
||||
details?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||
lastEvaluation: string;
|
||||
/** state describes the state of the lastEvaluation.
|
||||
It is limited to three possible states for machine evaluation. */
|
||||
state: 'success' | 'in_progress' | 'failed';
|
||||
};
|
||||
export type TemplateGroupStatus = {
|
||||
/** additionalFields is reserved for future use */
|
||||
additionalFields?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** operatorStates is a map of operator ID to operator state evaluations.
|
||||
Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||
operatorStates?: {
|
||||
[key: string]: TemplateGroupOperatorState;
|
||||
};
|
||||
};
|
||||
export type TemplateGroup = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion: string;
|
||||
@@ -1784,7 +1385,6 @@ export type TemplateGroup = {
|
||||
kind: string;
|
||||
metadata: ObjectMeta;
|
||||
spec: TemplateGroupSpec;
|
||||
status?: TemplateGroupStatus;
|
||||
};
|
||||
export type TemplateGroupList = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
@@ -1810,34 +1410,6 @@ export type TimeIntervalSpec = {
|
||||
name: string;
|
||||
time_intervals: TimeIntervalInterval[];
|
||||
};
|
||||
export type TimeIntervalOperatorState = {
|
||||
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
|
||||
descriptiveState?: string;
|
||||
/** details contains any extra information that is operator-specific */
|
||||
details?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** lastEvaluation is the ResourceVersion last evaluated */
|
||||
lastEvaluation: string;
|
||||
/** state describes the state of the lastEvaluation.
|
||||
It is limited to three possible states for machine evaluation. */
|
||||
state: 'success' | 'in_progress' | 'failed';
|
||||
};
|
||||
export type TimeIntervalStatus = {
|
||||
/** additionalFields is reserved for future use */
|
||||
additionalFields?: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
/** operatorStates is a map of operator ID to operator state evaluations.
|
||||
Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
|
||||
operatorStates?: {
|
||||
[key: string]: TimeIntervalOperatorState;
|
||||
};
|
||||
};
|
||||
export type TimeInterval = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion: string;
|
||||
@@ -1845,7 +1417,6 @@ export type TimeInterval = {
|
||||
kind: string;
|
||||
metadata: ObjectMeta;
|
||||
spec: TimeIntervalSpec;
|
||||
status?: TimeIntervalStatus;
|
||||
};
|
||||
export type TimeIntervalList = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
@@ -1855,3 +1426,43 @@ export type TimeIntervalList = {
|
||||
kind?: string;
|
||||
metadata: ListMeta;
|
||||
};
|
||||
export const {
|
||||
useGetApiResourcesQuery,
|
||||
useLazyGetApiResourcesQuery,
|
||||
useListReceiverQuery,
|
||||
useLazyListReceiverQuery,
|
||||
useCreateReceiverMutation,
|
||||
useDeletecollectionReceiverMutation,
|
||||
useGetReceiverQuery,
|
||||
useLazyGetReceiverQuery,
|
||||
useReplaceReceiverMutation,
|
||||
useDeleteReceiverMutation,
|
||||
useUpdateReceiverMutation,
|
||||
useListRoutingTreeQuery,
|
||||
useLazyListRoutingTreeQuery,
|
||||
useCreateRoutingTreeMutation,
|
||||
useDeletecollectionRoutingTreeMutation,
|
||||
useGetRoutingTreeQuery,
|
||||
useLazyGetRoutingTreeQuery,
|
||||
useReplaceRoutingTreeMutation,
|
||||
useDeleteRoutingTreeMutation,
|
||||
useUpdateRoutingTreeMutation,
|
||||
useListTemplateGroupQuery,
|
||||
useLazyListTemplateGroupQuery,
|
||||
useCreateTemplateGroupMutation,
|
||||
useDeletecollectionTemplateGroupMutation,
|
||||
useGetTemplateGroupQuery,
|
||||
useLazyGetTemplateGroupQuery,
|
||||
useReplaceTemplateGroupMutation,
|
||||
useDeleteTemplateGroupMutation,
|
||||
useUpdateTemplateGroupMutation,
|
||||
useListTimeIntervalQuery,
|
||||
useLazyListTimeIntervalQuery,
|
||||
useCreateTimeIntervalMutation,
|
||||
useDeletecollectionTimeIntervalMutation,
|
||||
useGetTimeIntervalQuery,
|
||||
useLazyGetTimeIntervalQuery,
|
||||
useReplaceTimeIntervalMutation,
|
||||
useDeleteTimeIntervalMutation,
|
||||
useUpdateTimeIntervalMutation,
|
||||
} = injectedRtkApi;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'rules.alerting.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'rulesAlertingAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from './api';
|
||||
import { api } from './baseAPI';
|
||||
export const addTagTypes = ['API Discovery', 'AlertRule', 'RecordingRule'] as const;
|
||||
const injectedRtkApi = api
|
||||
.enhanceEndpoints({
|
||||
@@ -7,7 +7,7 @@ const injectedRtkApi = api
|
||||
.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getApiResources: build.query<GetApiResourcesApiResponse, GetApiResourcesApiArg>({
|
||||
query: () => ({ url: `/apis/rules.alerting.grafana.app/v0alpha1/` }),
|
||||
query: () => ({ url: `/` }),
|
||||
providesTags: ['API Discovery'],
|
||||
}),
|
||||
listAlertRule: build.query<ListAlertRuleApiResponse, ListAlertRuleApiArg>({
|
||||
@@ -313,7 +313,7 @@ const injectedRtkApi = api
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as rulesAPI };
|
||||
export { injectedRtkApi as generatedAPI };
|
||||
export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList;
|
||||
export type GetApiResourcesApiArg = void;
|
||||
export type ListAlertRuleApiResponse = /** status 200 OK */ AlertRuleList;
|
||||
@@ -1085,3 +1085,33 @@ export type RecordingRuleList = {
|
||||
kind?: string;
|
||||
metadata: ListMeta;
|
||||
};
|
||||
export const {
|
||||
useGetApiResourcesQuery,
|
||||
useLazyGetApiResourcesQuery,
|
||||
useListAlertRuleQuery,
|
||||
useLazyListAlertRuleQuery,
|
||||
useCreateAlertRuleMutation,
|
||||
useDeletecollectionAlertRuleMutation,
|
||||
useGetAlertRuleQuery,
|
||||
useLazyGetAlertRuleQuery,
|
||||
useReplaceAlertRuleMutation,
|
||||
useDeleteAlertRuleMutation,
|
||||
useUpdateAlertRuleMutation,
|
||||
useGetAlertRuleStatusQuery,
|
||||
useLazyGetAlertRuleStatusQuery,
|
||||
useReplaceAlertRuleStatusMutation,
|
||||
useUpdateAlertRuleStatusMutation,
|
||||
useListRecordingRuleQuery,
|
||||
useLazyListRecordingRuleQuery,
|
||||
useCreateRecordingRuleMutation,
|
||||
useDeletecollectionRecordingRuleMutation,
|
||||
useGetRecordingRuleQuery,
|
||||
useLazyGetRecordingRuleQuery,
|
||||
useReplaceRecordingRuleMutation,
|
||||
useDeleteRecordingRuleMutation,
|
||||
useUpdateRecordingRuleMutation,
|
||||
useGetRecordingRuleStatusQuery,
|
||||
useLazyGetRecordingRuleStatusQuery,
|
||||
useReplaceRecordingRuleStatusMutation,
|
||||
useUpdateRecordingRuleStatusMutation,
|
||||
} = injectedRtkApi;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -1 +1,4 @@
|
||||
export { getAPINamespace, getAPIBaseURL, normalizeError, handleRequestError } from './utils/utils';
|
||||
|
||||
/* @TODO figure out how to automatically set the MockBackendSrv when consumers of this package write tests using the exported clients */
|
||||
export { MockBackendSrv } from './utils/backendSrv.mock';
|
||||
|
||||
@@ -108,6 +108,8 @@ const config: ConfigFile = {
|
||||
...createAPIConfig('preferences', 'v1alpha1'),
|
||||
...createAPIConfig('provisioning', 'v0alpha1'),
|
||||
...createAPIConfig('shorturl', 'v1beta1'),
|
||||
...createAPIConfig('notifications.alerting', 'v0alpha1'),
|
||||
...createAPIConfig('rules.alerting', 'v0alpha1'),
|
||||
...createAPIConfig('historian.alerting', 'v0alpha1'),
|
||||
...createAPIConfig('logsdrilldown', 'v1alpha1'),
|
||||
// PLOP_INJECT_API_CLIENT - Used by the API client generator
|
||||
|
||||
46
packages/grafana-api-clients/src/utils/backendSrv.mock.ts
Normal file
46
packages/grafana-api-clients/src/utils/backendSrv.mock.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { fromFetch } from 'rxjs/fetch';
|
||||
|
||||
import { BackendSrv, BackendSrvRequest, FetchResponse } from '@grafana/runtime';
|
||||
|
||||
/**
|
||||
* Minimal mock implementation of BackendSrv for testing.
|
||||
* Only implements the fetch() method which is used by RTKQ.
|
||||
* HTTP requests are intercepted by MSW in tests.
|
||||
*/
|
||||
export class MockBackendSrv implements Partial<BackendSrv> {
|
||||
fetch<T>(options: BackendSrvRequest): Observable<FetchResponse<T>> {
|
||||
const init: RequestInit = {
|
||||
method: options.method || 'GET',
|
||||
headers: options.headers,
|
||||
body: options.data ? JSON.stringify(options.data) : undefined,
|
||||
credentials: options.credentials,
|
||||
signal: options.abortSignal,
|
||||
};
|
||||
|
||||
return new Observable((observer) => {
|
||||
fromFetch(options.url, init).subscribe({
|
||||
next: async (response) => {
|
||||
try {
|
||||
const data = await response.json();
|
||||
observer.next({
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
headers: response.headers,
|
||||
redirected: response.redirected,
|
||||
type: response.type,
|
||||
url: response.url,
|
||||
config: options,
|
||||
});
|
||||
observer.complete();
|
||||
} catch (error) {
|
||||
observer.error(error);
|
||||
}
|
||||
},
|
||||
error: (error) => observer.error(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1259,4 +1259,8 @@ export interface FeatureToggles {
|
||||
* Enables the ASAP smoothing transformation for time series data
|
||||
*/
|
||||
smoothingTransformation?: boolean;
|
||||
/**
|
||||
* Enables the creation of keepers that manage secrets stored on AWS secrets manager
|
||||
*/
|
||||
secretsManagementAppPlatformAwsKeeper?: boolean;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
_ "sigs.k8s.io/randfill"
|
||||
_ "xorm.io/builder"
|
||||
|
||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
_ "github.com/grafana/authlib/authn"
|
||||
_ "github.com/grafana/authlib/authz"
|
||||
_ "github.com/grafana/authlib/cache"
|
||||
|
||||
@@ -2,54 +2,84 @@ package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// loggerFactory is a function that creates a Logger given a name.
|
||||
// It can be set by calling SetLoggerFactory to use a custom logger implementation.
|
||||
var loggerFactory func(name string) Logger
|
||||
|
||||
// SetLoggerFactory sets the factory function used to create loggers.
|
||||
// This should be called during initialization to register a custom logger implementation.
|
||||
// If not set, a default slog-based logger will be used.
|
||||
func SetLoggerFactory(factory func(name string) Logger) {
|
||||
loggerFactory = factory
|
||||
}
|
||||
|
||||
var slogLogManager = &slogLoggerManager{
|
||||
cache: sync.Map{},
|
||||
}
|
||||
|
||||
func New(name string) Logger {
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: log.New(name),
|
||||
if loggerFactory != nil {
|
||||
return loggerFactory(name)
|
||||
}
|
||||
// add a caching layer since slog doesn't perform any caching itself
|
||||
return slogLogManager.getOrCreate(name)
|
||||
}
|
||||
|
||||
type grafanaInfraLogWrapper struct {
|
||||
l *log.ConcreteLogger
|
||||
type slogLoggerManager struct {
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) New(ctx ...any) Logger {
|
||||
func (m *slogLoggerManager) getOrCreate(name string) Logger {
|
||||
if cached, ok := m.cache.Load(name); ok {
|
||||
return cached.(*slogLogger)
|
||||
}
|
||||
|
||||
logger := &slogLogger{
|
||||
logger: slog.Default().With("logger", name),
|
||||
name: name,
|
||||
}
|
||||
actual, _ := m.cache.LoadOrStore(name, logger)
|
||||
return actual.(*slogLogger)
|
||||
}
|
||||
|
||||
type slogLogger struct {
|
||||
logger *slog.Logger
|
||||
name string
|
||||
}
|
||||
|
||||
func (l *slogLogger) New(ctx ...any) Logger {
|
||||
if len(ctx) == 0 {
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: d.l.New(),
|
||||
return &slogLogger{
|
||||
logger: l.logger,
|
||||
name: l.name,
|
||||
}
|
||||
}
|
||||
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: d.l.New(ctx...),
|
||||
return &slogLogger{
|
||||
logger: l.logger.With(ctx...),
|
||||
name: l.name,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Debug(msg string, ctx ...any) {
|
||||
d.l.Debug(msg, ctx...)
|
||||
func (l *slogLogger) Debug(msg string, ctx ...any) {
|
||||
l.logger.Debug(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Info(msg string, ctx ...any) {
|
||||
d.l.Info(msg, ctx...)
|
||||
func (l *slogLogger) Info(msg string, ctx ...any) {
|
||||
l.logger.Info(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Warn(msg string, ctx ...any) {
|
||||
d.l.Warn(msg, ctx...)
|
||||
func (l *slogLogger) Warn(msg string, ctx ...any) {
|
||||
l.logger.Warn(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Error(msg string, ctx ...any) {
|
||||
d.l.Error(msg, ctx...)
|
||||
func (l *slogLogger) Error(msg string, ctx ...any) {
|
||||
l.logger.Error(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) FromContext(ctx context.Context) Logger {
|
||||
concreteInfraLogger, ok := d.l.FromContext(ctx).(*log.ConcreteLogger)
|
||||
if !ok {
|
||||
return d.New()
|
||||
}
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: concreteInfraLogger,
|
||||
}
|
||||
func (l *slogLogger) FromContext(_ context.Context) Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
@@ -170,6 +170,13 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *collections.Star
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send an error if we try to save a non-dashboard star
|
||||
for _, res := range obj.Spec.Resource {
|
||||
if res.Group != "dashboard.grafana.app" || res.Kind != "Dashboard" {
|
||||
return nil, fmt.Errorf("only dashboard stars are supported until the migration to unified storage is complete")
|
||||
}
|
||||
}
|
||||
|
||||
user, err := s.users.GetByUID(ctx, &user.GetUserByUIDQuery{
|
||||
UID: owner.Identifier,
|
||||
})
|
||||
|
||||
@@ -276,7 +276,7 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if b.providerType == setting.GOFFProviderType {
|
||||
if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType {
|
||||
b.proxyFlagReq(ctx, flagKey, isAuthedReq, w, r)
|
||||
return
|
||||
}
|
||||
@@ -304,7 +304,7 @@ func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
isAuthedReq := b.isAuthenticatedRequest(r)
|
||||
span.SetAttributes(attribute.Bool("authenticated", isAuthedReq))
|
||||
|
||||
if b.providerType == setting.GOFFProviderType {
|
||||
if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType {
|
||||
b.proxyAllFlagReq(ctx, isAuthedReq, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
const (
|
||||
// This constant can be used as a key in resource tags
|
||||
GrafanaSecretsManagerName = "grafana-secrets-manager"
|
||||
)
|
||||
|
||||
var (
|
||||
// The name used to refer to the system keeper
|
||||
SystemKeeperName = "system"
|
||||
@@ -102,8 +107,8 @@ func (s ExternalID) String() string {
|
||||
// Keeper is the interface for secret keepers.
|
||||
type Keeper interface {
|
||||
Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error)
|
||||
Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error
|
||||
Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error)
|
||||
RetrieveReference(ctx context.Context, cfg secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error)
|
||||
Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ type DecryptSecureValue struct {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrSecureValueNotFound = errors.New("secure value not found")
|
||||
ErrSecureValueAlreadyExists = errors.New("secure value already exists")
|
||||
ErrSecureValueNotFound = errors.New("secure value not found")
|
||||
ErrSecureValueAlreadyExists = errors.New("secure value already exists")
|
||||
ErrReferenceWithSystemKeeper = errors.New("tried to create secure value using reference with system keeper, references can only be used with 3rd party keepers")
|
||||
ErrSecureValueMissingSecretAndRef = errors.New("secure value spec doesn't have neither a secret or reference")
|
||||
)
|
||||
|
||||
type ReadOpts struct {
|
||||
|
||||
@@ -103,9 +103,12 @@ func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) err
|
||||
return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Status.Keeper, err)
|
||||
}
|
||||
|
||||
// Keeper deletion is idempotent
|
||||
if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil {
|
||||
return fmt.Errorf("deleting secure value from keeper: %w", err)
|
||||
// If the secure value doesn't use a reference, delete the secret
|
||||
if sv.Spec.Ref == nil {
|
||||
// Keeper deletion is idempotent
|
||||
if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil {
|
||||
return fmt.Errorf("deleting secure value from keeper: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata deletion is not idempotent but not found errors are ignored
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package garbagecollectionworker_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -97,27 +96,33 @@ func TestBasic(t *testing.T) {
|
||||
require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv))
|
||||
require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv))
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"})
|
||||
nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"})
|
||||
namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"})
|
||||
anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
return &secretv1beta1.SecureValue{
|
||||
t.Run("cleaning up secure values that use references", func(t *testing.T) {
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
keeper, err := sut.CreateAWSKeeper(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name))
|
||||
|
||||
sv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: nameGen.Draw(t, "name"),
|
||||
Namespace: namespaceGen.Draw(t, "ns"),
|
||||
Namespace: keeper.Namespace,
|
||||
Name: "sv1",
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"),
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))),
|
||||
Decrypters: rapid.SliceOfDistinct(decryptersGen, func(v string) string { return v }).Draw(t, "decrypters"),
|
||||
Description: "desc1",
|
||||
Ref: ptr.To("ref1"),
|
||||
Decrypters: []string{"decrypter1"},
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sut.DeleteSv(t.Context(), sv.Namespace, sv.Name)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
func TestProperty(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -126,26 +131,59 @@ func TestProperty(t *testing.T) {
|
||||
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
sut := testutils.Setup(tt)
|
||||
model := newModel()
|
||||
model := testutils.NewModelGsm(nil)
|
||||
|
||||
t.Repeat(map[string]func(*rapid.T){
|
||||
"create": func(t *rapid.T) {
|
||||
sv := anySecureValueGen.Draw(t, "sv")
|
||||
var sv *secretv1beta1.SecureValue
|
||||
if rapid.Bool().Draw(t, "withRef") {
|
||||
sv = testutils.AnySecureValueWithRefGen.Draw(t, "sv")
|
||||
} else {
|
||||
sv = testutils.AnySecureValueGen.Draw(t, "sv")
|
||||
}
|
||||
|
||||
svCopy := sv.DeepCopy()
|
||||
|
||||
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv))
|
||||
svCopy.UID = createdSv.UID
|
||||
modelErr := model.create(sut.Clock.Now(), svCopy)
|
||||
if err == nil {
|
||||
svCopy.UID = createdSv.UID
|
||||
}
|
||||
_, modelErr := model.Create(sut.Clock.Now(), svCopy)
|
||||
require.ErrorIs(t, err, modelErr)
|
||||
},
|
||||
"createKeeper": func(t *rapid.T) {
|
||||
input := testutils.AnyKeeperGen.Draw(t, "keeper")
|
||||
modelKeeper, modelErr := model.CreateKeeper(input)
|
||||
keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), input, "actor-uid")
|
||||
if err != nil || modelErr != nil {
|
||||
require.ErrorIs(t, err, modelErr)
|
||||
return
|
||||
}
|
||||
require.Equal(t, modelKeeper.Name, keeper.Name)
|
||||
},
|
||||
"setKeeperAsActive": func(t *rapid.T) {
|
||||
namespace := testutils.NamespaceGen.Draw(t, "namespace")
|
||||
var keeper string
|
||||
if rapid.Bool().Draw(t, "systemKeeper") {
|
||||
keeper = contracts.SystemKeeperName
|
||||
} else {
|
||||
keeper = testutils.KeeperNameGen.Draw(t, "keeper")
|
||||
}
|
||||
modelErr := model.SetKeeperAsActive(namespace, keeper)
|
||||
err := sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), keeper)
|
||||
if err != nil || modelErr != nil {
|
||||
require.ErrorIs(t, err, modelErr)
|
||||
return
|
||||
}
|
||||
},
|
||||
"delete": func(t *rapid.T) {
|
||||
if len(model.items) == 0 {
|
||||
if len(model.SecureValues) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
i := rapid.IntRange(0, len(model.items)-1).Draw(t, "index")
|
||||
sv := model.items[i]
|
||||
modelErr := model.delete(sv.Namespace, sv.Name)
|
||||
i := rapid.IntRange(0, len(model.SecureValues)-1).Draw(t, "index")
|
||||
sv := model.SecureValues[i]
|
||||
_, modelErr := model.Delete(sv.Namespace, sv.Name)
|
||||
_, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name)
|
||||
require.ErrorIs(t, err, modelErr)
|
||||
},
|
||||
@@ -153,7 +191,7 @@ func TestProperty(t *testing.T) {
|
||||
// Taken from secureValueMetadataStorage.acquireLeases
|
||||
minAge := 300 * time.Second
|
||||
maxBatchSize := sut.GarbageCollectionWorker.Cfg.SecretsManagement.GCWorkerMaxBatchSize
|
||||
modelDeleted, modelErr := model.cleanupInactiveSecureValues(sut.Clock.Now(), minAge, maxBatchSize)
|
||||
modelDeleted, modelErr := model.CleanupInactiveSecureValues(sut.Clock.Now(), minAge, maxBatchSize)
|
||||
deleted, err := sut.GarbageCollectionWorker.CleanupInactiveSecureValues(t.Context())
|
||||
require.ErrorIs(t, err, modelErr)
|
||||
|
||||
@@ -174,77 +212,3 @@ func TestProperty(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type model struct {
|
||||
items []*modelSecureValue
|
||||
}
|
||||
|
||||
type modelSecureValue struct {
|
||||
*secretv1beta1.SecureValue
|
||||
active bool
|
||||
created time.Time
|
||||
}
|
||||
|
||||
func newModel() *model {
|
||||
return &model{
|
||||
items: make([]*modelSecureValue, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) error {
|
||||
created := now
|
||||
for _, item := range m.items {
|
||||
if item.active && item.Namespace == sv.Namespace && item.Name == sv.Name {
|
||||
item.active = false
|
||||
created = item.created
|
||||
break
|
||||
}
|
||||
}
|
||||
m.items = append(m.items, &modelSecureValue{SecureValue: sv, active: true, created: created})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) delete(ns string, name string) error {
|
||||
for _, sv := range m.items {
|
||||
if sv.active && sv.Namespace == ns && sv.Name == name {
|
||||
sv.active = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return contracts.ErrSecureValueNotFound
|
||||
}
|
||||
|
||||
func (m *model) cleanupInactiveSecureValues(now time.Time, minAge time.Duration, maxBatchSize uint16) ([]*modelSecureValue, error) {
|
||||
// Using a slice to allow duplicates
|
||||
toDelete := make([]*modelSecureValue, 0)
|
||||
|
||||
// The implementation query sorts by created time ascending
|
||||
slices.SortFunc(m.items, func(a, b *modelSecureValue) int {
|
||||
if a.created.Before(b.created) {
|
||||
return -1
|
||||
} else if a.created.After(b.created) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, sv := range m.items {
|
||||
if len(toDelete) >= int(maxBatchSize) {
|
||||
break
|
||||
}
|
||||
|
||||
if !sv.active && now.Sub(sv.created) > minAge {
|
||||
toDelete = append(toDelete, sv)
|
||||
}
|
||||
}
|
||||
|
||||
// PERF: The slices are always small
|
||||
m.items = slices.DeleteFunc(m.items, func(v1 *modelSecureValue) bool {
|
||||
return slices.ContainsFunc(toDelete, func(v2 *modelSecureValue) bool {
|
||||
return v2.UID == v1.UID
|
||||
})
|
||||
})
|
||||
|
||||
return toDelete, nil
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig,
|
||||
return exposedValue, nil
|
||||
}
|
||||
|
||||
func (s *SQLKeeper) RetrieveReference(ctx context.Context, cfg secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) {
|
||||
return "", fmt.Errorf("reference is not implemented by the SQLKeeper")
|
||||
}
|
||||
|
||||
func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error {
|
||||
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes(
|
||||
attribute.String("namespace", namespace.String()),
|
||||
@@ -125,27 +129,3 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error {
|
||||
ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes(
|
||||
attribute.String("namespace", namespace.String()),
|
||||
attribute.String("name", name),
|
||||
attribute.Int64("version", version),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
encryptedData, err := s.encryptionManager.Encrypt(ctx, namespace, []byte(exposedValueOrRef))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encrypt value: %w", err)
|
||||
}
|
||||
|
||||
err = s.store.Update(ctx, namespace, name, version, encryptedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update encrypted value: %w", err)
|
||||
}
|
||||
|
||||
s.metrics.UpdateDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func Test_SQLKeeperSetup(t *testing.T) {
|
||||
plaintext1 := "very secret string in namespace 1"
|
||||
plaintext2 := "very secret string in namespace 2"
|
||||
|
||||
keeperCfg := &secretv1beta1.SystemKeeperConfig{}
|
||||
keeperCfg := secretv1beta1.NewNamedKeeperConfig("k1", &secretv1beta1.SystemKeeperConfig{})
|
||||
|
||||
t.Run("storing an encrypted value returns no error", func(t *testing.T) {
|
||||
sut := testutils.Setup(t)
|
||||
@@ -123,31 +123,6 @@ func Test_SQLKeeperSetup(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("updating an existent encrypted value returns no error", func(t *testing.T) {
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sut.SQLKeeper.Update(t.Context(), keeperCfg, namespace1, name1, version1, plaintext2)
|
||||
require.NoError(t, err)
|
||||
|
||||
exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, exposedVal)
|
||||
assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue())
|
||||
})
|
||||
|
||||
t.Run("updating a non existent encrypted value returns error", func(t *testing.T) {
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
_, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sut.SQLKeeper.Update(t.Context(), nil, namespace1, "non_existing_name", version1, plaintext2)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("data key migration only runs if both secrets db migrations are enabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv
|
||||
return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeper: %q %w", newSecureValue.Namespace, currentVersion.Status.Keeper, err)
|
||||
}
|
||||
|
||||
if newSecureValue.Spec.Value == nil {
|
||||
if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil {
|
||||
keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Status.Keeper, err)
|
||||
@@ -150,7 +150,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv
|
||||
|
||||
secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("reading secret value from keeper: %w", err)
|
||||
return nil, false, fmt.Errorf("reading secret value from keeper: %w %w", contracts.ErrSecureValueMissingSecretAndRef, err)
|
||||
}
|
||||
|
||||
newSecureValue.Spec.Value = &secret
|
||||
@@ -174,6 +174,10 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName st
|
||||
return nil, contracts.NewErrValidateSecureValue(errorList)
|
||||
}
|
||||
|
||||
if sv.Spec.Ref != nil && keeperCfg.Type() == secretv1beta1.SystemKeeperType {
|
||||
return nil, contracts.ErrReferenceWithSystemKeeper
|
||||
}
|
||||
|
||||
createdSv, err := s.secureValueMetadataStorage.Create(ctx, keeperName, sv, actorUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating secure value: %w", err)
|
||||
@@ -189,18 +193,28 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName st
|
||||
return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, keeperName, err)
|
||||
}
|
||||
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "type", keeperCfg.Type())
|
||||
|
||||
// TODO: can we stop using external id?
|
||||
// TODO: store uses only the namespace and returns and id. It could be a kv instead.
|
||||
// TODO: check that the encrypted store works with multiple versions
|
||||
externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storing secure value in keeper: %w", err)
|
||||
}
|
||||
createdSv.Status.ExternalID = string(externalID)
|
||||
switch {
|
||||
case sv.Spec.Value != nil:
|
||||
externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storing secure value in keeper: %w", err)
|
||||
}
|
||||
createdSv.Status.ExternalID = string(externalID)
|
||||
|
||||
if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil {
|
||||
return nil, fmt.Errorf("setting secure value external id: %w", err)
|
||||
if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil {
|
||||
return nil, fmt.Errorf("setting secure value external id: %w", err)
|
||||
}
|
||||
|
||||
case sv.Spec.Ref != nil:
|
||||
// No-op, there's nothing to store in the keeper since the
|
||||
// secret is already stored in the 3rd party secret store
|
||||
// and it's being referenced.
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("secure value doesn't specify either a secret value or a reference")
|
||||
}
|
||||
|
||||
if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil {
|
||||
@@ -366,3 +380,20 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error {
|
||||
// The system keeper is not in the database, so skip checking it exists.
|
||||
// TODO: should the system keeper be in the database?
|
||||
if name != contracts.SystemKeeperName {
|
||||
// Check keeper exists. No need to worry about time of check to time of use
|
||||
// since trying to activate a just deleted keeper will result in all
|
||||
// keepers being inactive and defaulting to the system keeper.
|
||||
if _, err := s.keeperMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{}); err != nil {
|
||||
return fmt.Errorf("reading keeper before setting as active: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil {
|
||||
return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
@@ -93,4 +94,149 @@ func TestCrud(t *testing.T) {
|
||||
_, err = sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name, contracts.ReadOpts{})
|
||||
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
||||
})
|
||||
|
||||
t.Run("secret can be referenced only when the active keeper is a 3rd party keeper", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
ref := "path-to-secret"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sv1",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "desc1",
|
||||
Ref: &ref,
|
||||
Decrypters: []string{"decrypter1"},
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
|
||||
// Creating a secure value using ref with the system keeper
|
||||
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv))
|
||||
require.NotNil(t, err)
|
||||
require.Nil(t, createdSv)
|
||||
require.Contains(t, err.Error(), "tried to create secure value using reference with system keeper, references can only be used with 3rd party keepers")
|
||||
|
||||
// Create a 3rd party keeper
|
||||
keeper := &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k1",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "desc",
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{
|
||||
Region: "us-east-1",
|
||||
AssumeRole: &secretv1beta1.KeeperAWSAssumeRole{
|
||||
AssumeRoleArn: "arn",
|
||||
ExternalID: "id",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create a 3rd party keeper
|
||||
_, err = sut.KeeperMetadataStorage.Create(t.Context(), keeper, "actor-uid")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set the new keeper as active
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name))
|
||||
|
||||
// Create a secure value using a ref
|
||||
createdSv, err = sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, keeper.Name, createdSv.Status.Keeper)
|
||||
})
|
||||
|
||||
t.Run("creating secure value with reference", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
// Create a keeper because references cannot be used with the system keeper
|
||||
keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns",
|
||||
Name: "k1",
|
||||
},
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
},
|
||||
}, "actor-uid")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name))
|
||||
|
||||
sv, err := sut.CreateSv(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sv)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_SetAsActive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("setting the system keeper as the active keeper", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
namespace := "ns"
|
||||
|
||||
// Create a new keeper
|
||||
keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns",
|
||||
Name: "k1",
|
||||
},
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
},
|
||||
}, "actor-uid")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set the new keeper as active
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name))
|
||||
keeperName, _, err := sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), namespace)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, keeper.Name, keeperName)
|
||||
|
||||
// Set the system keeper as active
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), contracts.SystemKeeperName))
|
||||
keeperName, _, err = sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), namespace)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contracts.SystemKeeperName, keeperName)
|
||||
})
|
||||
|
||||
t.Run("each namespace can have one active keeper", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
k1, err := sut.CreateKeeper(t.Context(), func(ckc *testutils.CreateKeeperConfig) {
|
||||
ckc.Keeper.Namespace = "ns1"
|
||||
ckc.Keeper.Name = "k1"
|
||||
})
|
||||
require.NoError(t, err)
|
||||
k2, err := sut.CreateKeeper(t.Context(), func(ckc *testutils.CreateKeeperConfig) {
|
||||
ckc.Keeper.Namespace = "ns2"
|
||||
ckc.Keeper.Name = "k2"
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name))
|
||||
require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name))
|
||||
|
||||
keeperName, _, err := sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k1.Namespace)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, k1.Name, keeperName)
|
||||
|
||||
keeperName, _, err = sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k2.Namespace)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, k2.Name, keeperName)
|
||||
})
|
||||
}
|
||||
|
||||
96
pkg/registry/apis/secret/testutils/generators.go
Normal file
96
pkg/registry/apis/secret/testutils/generators.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
var (
|
||||
DecryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"})
|
||||
SecureValueNameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"})
|
||||
KeeperNameGen = rapid.SampledFrom([]string{"k1", "k2", "k3", "k4", "k5"})
|
||||
NamespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"})
|
||||
SecretsToRefGen = rapid.SampledFrom([]string{"ref1", "ref2", "ref3", "ref4", "ref5"})
|
||||
// Generator for secure values that specify a secret value
|
||||
AnySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
return &secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"),
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))),
|
||||
Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"),
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
})
|
||||
// Generator for secure values that reference values from 3rd party stores
|
||||
AnySecureValueWithRefGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
return &secretv1beta1.SecureValue{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"),
|
||||
Ref: ptr.To(SecretsToRefGen.Draw(t, "ref")),
|
||||
Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"),
|
||||
},
|
||||
Status: secretv1beta1.SecureValueStatus{},
|
||||
}
|
||||
})
|
||||
UpdateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue {
|
||||
sv := AnySecureValueGen.Draw(t, "sv")
|
||||
// Maybe update the secret value, maybe not
|
||||
if !rapid.Bool().Draw(t, "should_update_value") {
|
||||
sv.Spec.Value = nil
|
||||
}
|
||||
return sv
|
||||
})
|
||||
DecryptGen = rapid.Custom(func(t *rapid.T) DecryptInput {
|
||||
return DecryptInput{
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
Name: SecureValueNameGen.Draw(t, "name"),
|
||||
Decrypter: DecryptersGen.Draw(t, "decrypter"),
|
||||
}
|
||||
})
|
||||
AnyKeeperGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.Keeper {
|
||||
spec := secretv1beta1.KeeperSpec{
|
||||
Description: rapid.String().Draw(t, "description"),
|
||||
}
|
||||
|
||||
keeperType := rapid.SampledFrom([]string{"isAwsKeeper", "isAzureKeeper", "isGcpKeeper", "isVaultKeeper"}).Draw(t, "keeperType")
|
||||
switch keeperType {
|
||||
case "isAwsKeeper":
|
||||
spec.Aws = &secretv1beta1.KeeperAWSConfig{}
|
||||
case "isAzureKeeper":
|
||||
spec.Azure = &secretv1beta1.KeeperAzureConfig{}
|
||||
case "isGcpKeeper":
|
||||
spec.Gcp = &secretv1beta1.KeeperGCPConfig{}
|
||||
case "isVaultKeeper":
|
||||
spec.HashiCorpVault = &secretv1beta1.KeeperHashiCorpConfig{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled keeper type '%+v', did you forget a switch case?", keeperType))
|
||||
}
|
||||
|
||||
return &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: KeeperNameGen.Draw(t, "name"),
|
||||
Namespace: NamespaceGen.Draw(t, "ns"),
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
type DecryptInput struct {
|
||||
Namespace string
|
||||
Name string
|
||||
Decrypter string
|
||||
}
|
||||
321
pkg/registry/apis/secret/testutils/model_gsm.go
Normal file
321
pkg/registry/apis/secret/testutils/model_gsm.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
type ModelSecureValue struct {
|
||||
*secretv1beta1.SecureValue
|
||||
active bool
|
||||
created time.Time
|
||||
leaseCreated time.Time
|
||||
}
|
||||
|
||||
type ModelKeeper struct {
|
||||
namespace string
|
||||
name string
|
||||
active bool
|
||||
keeperType secretv1beta1.KeeperType
|
||||
}
|
||||
|
||||
// A simplified in memoruy model of the grafana secrets manager
|
||||
type ModelGsm struct {
|
||||
SecureValues []*ModelSecureValue
|
||||
Keepers []*ModelKeeper
|
||||
modelSecretsManager *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
func NewModelGsm(modelSecretsManager *ModelAWSSecretsManager) *ModelGsm {
|
||||
return &ModelGsm{modelSecretsManager: modelSecretsManager}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) getNewVersionNumber(namespace, name string) int64 {
|
||||
latestVersion := int64(0)
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name {
|
||||
latestVersion = max(latestVersion, sv.Status.Version)
|
||||
}
|
||||
}
|
||||
return latestVersion + 1
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetVersionToActive(namespace, name string, version int64) {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name {
|
||||
sv.active = sv.Status.Version == version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetVersionToInactive(namespace, name string, version int64) {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version {
|
||||
sv.active = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) ReadActiveVersion(namespace, name string) *ModelSecureValue {
|
||||
for _, sv := range m.SecureValues {
|
||||
if sv.Namespace == namespace && sv.Name == name && sv.active {
|
||||
return sv
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) {
|
||||
keeper := m.getActiveKeeper(sv.Namespace)
|
||||
|
||||
if sv.Spec.Ref != nil && keeper.keeperType == secretv1beta1.SystemKeeperType {
|
||||
return nil, contracts.ErrReferenceWithSystemKeeper
|
||||
}
|
||||
|
||||
sv = sv.DeepCopy()
|
||||
|
||||
// Preserve the original creation time if this secure value already exists
|
||||
created := now
|
||||
if sv := m.ReadActiveVersion(sv.Namespace, sv.Name); sv != nil {
|
||||
created = sv.created
|
||||
}
|
||||
|
||||
modelSv := &ModelSecureValue{SecureValue: sv, active: false, created: created}
|
||||
modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name)
|
||||
modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version)
|
||||
modelSv.Status.Keeper = keeper.name
|
||||
m.SecureValues = append(m.SecureValues, modelSv)
|
||||
m.SetVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version)
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) getActiveKeeper(namespace string) *ModelKeeper {
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace && k.active {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the system keeper when there are no active keepers in the namespace
|
||||
return &ModelKeeper{
|
||||
namespace: namespace,
|
||||
name: contracts.SystemKeeperName,
|
||||
active: true,
|
||||
keeperType: secretv1beta1.SystemKeeperType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelGsm) keeperExists(namespace, name string) bool {
|
||||
return m.findKeeper(namespace, name) != nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) findKeeper(namespace, name string) *ModelKeeper {
|
||||
// The system keeper is not in the list of keepers
|
||||
if name == contracts.SystemKeeperName {
|
||||
return &ModelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true, keeperType: secretv1beta1.SystemKeeperType}
|
||||
}
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace && k.name == name {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) CreateKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) {
|
||||
if m.keeperExists(keeper.Namespace, keeper.Name) {
|
||||
return nil, contracts.ErrKeeperAlreadyExists
|
||||
}
|
||||
|
||||
var keeperType secretv1beta1.KeeperType
|
||||
switch {
|
||||
case keeper.Spec.Aws != nil:
|
||||
keeperType = secretv1beta1.AWSKeeperType
|
||||
case keeper.Spec.Gcp != nil:
|
||||
keeperType = secretv1beta1.GCPKeeperType
|
||||
case keeper.Spec.Azure != nil:
|
||||
keeperType = secretv1beta1.AzureKeeperType
|
||||
case keeper.Spec.HashiCorpVault != nil:
|
||||
keeperType = secretv1beta1.HashiCorpKeeperType
|
||||
default:
|
||||
keeperType = secretv1beta1.SystemKeeperType
|
||||
}
|
||||
|
||||
m.Keepers = append(m.Keepers, &ModelKeeper{namespace: keeper.Namespace, name: keeper.Name, keeperType: keeperType})
|
||||
|
||||
return keeper.DeepCopy(), nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) SetKeeperAsActive(namespace, keeperName string) error {
|
||||
// Set every other keeper in the namespace as inactive
|
||||
for _, k := range m.Keepers {
|
||||
if k.namespace == namespace {
|
||||
k.active = k.name == keeperName
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) {
|
||||
sv := m.ReadActiveVersion(newSecureValue.Namespace, newSecureValue.Name)
|
||||
if sv == nil {
|
||||
return nil, false, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
|
||||
// If the keeper doesn't exist, return an error
|
||||
if !m.keeperExists(sv.Namespace, sv.Status.Keeper) {
|
||||
return nil, false, contracts.ErrKeeperNotFound
|
||||
}
|
||||
|
||||
// If the payload doesn't contain a value and it's not using a reference, get the value from current version
|
||||
if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil {
|
||||
// Tried to update a secure value without providing a new value or a ref
|
||||
if sv.Spec.Value == nil {
|
||||
return nil, false, contracts.ErrSecureValueMissingSecretAndRef
|
||||
}
|
||||
newSecureValue.Spec.Value = sv.Spec.Value
|
||||
}
|
||||
|
||||
createdSv, err := m.Create(now, newSecureValue)
|
||||
|
||||
return createdSv, true, err
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Delete(namespace, name string) (*secretv1beta1.SecureValue, error) {
|
||||
modelSv := m.ReadActiveVersion(namespace, name)
|
||||
if modelSv == nil {
|
||||
return nil, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
m.SetVersionToInactive(namespace, name, modelSv.Status.Version)
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) List(namespace string) (*secretv1beta1.SecureValueList, error) {
|
||||
out := make([]secretv1beta1.SecureValue, 0)
|
||||
|
||||
for _, v := range m.SecureValues {
|
||||
if v.Namespace == namespace && v.active {
|
||||
out = append(out, *v.SecureValue)
|
||||
}
|
||||
}
|
||||
|
||||
return &secretv1beta1.SecureValueList{Items: out}, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Decrypt(ctx context.Context, decrypter, namespace, name string) (map[string]decrypt.DecryptResult, error) {
|
||||
for _, v := range m.SecureValues {
|
||||
if v.Namespace == namespace &&
|
||||
v.Name == name &&
|
||||
v.active {
|
||||
if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) {
|
||||
switch {
|
||||
// It's a secure value that specifies the secret
|
||||
case v.Spec.Value != nil:
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value),
|
||||
}, nil
|
||||
|
||||
// It's a secure value that references a secret on a 3rd party store
|
||||
case v.Spec.Ref != nil:
|
||||
keeper := m.findKeeper(v.Namespace, v.Status.Keeper)
|
||||
switch keeper.keeperType {
|
||||
case secretv1beta1.AWSKeeperType:
|
||||
exposedValue, err := m.modelSecretsManager.RetrieveReference(ctx, nil, *v.Spec.Ref)
|
||||
if err != nil {
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(fmt.Errorf("%w: %w", contracts.ErrDecryptFailed, err)),
|
||||
}, nil
|
||||
}
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultValue(&exposedValue),
|
||||
}, nil
|
||||
|
||||
// Other keepers are not implemented so we default to the system keeper
|
||||
default:
|
||||
// The system keeper doesn't implement Reference so decryption always fails
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptFailed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
default:
|
||||
panic("bug: secure value where Spec.Value and Spec.Ref are nil")
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return map[string]decrypt.DecryptResult{
|
||||
name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotFound),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) Read(namespace, name string) (*secretv1beta1.SecureValue, error) {
|
||||
modelSv := m.ReadActiveVersion(namespace, name)
|
||||
if modelSv == nil {
|
||||
return nil, contracts.ErrSecureValueNotFound
|
||||
}
|
||||
return modelSv.SecureValue, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) LeaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) {
|
||||
out := make([]*ModelSecureValue, 0)
|
||||
|
||||
for _, sv := range m.SecureValues {
|
||||
if len(out) >= int(maxBatchSize) {
|
||||
break
|
||||
}
|
||||
if !sv.active && now.Sub(sv.created) > minAge && now.Sub(sv.leaseCreated) > leaseTTL {
|
||||
sv.leaseCreated = now
|
||||
out = append(out, sv)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *ModelGsm) CleanupInactiveSecureValues(now time.Time, minAge time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) {
|
||||
// Using a slice to allow duplicates
|
||||
toDelete := make([]*ModelSecureValue, 0)
|
||||
|
||||
// The implementation query sorts by created time ascending
|
||||
slices.SortFunc(m.SecureValues, func(a, b *ModelSecureValue) int {
|
||||
if a.created.Before(b.created) {
|
||||
return -1
|
||||
} else if a.created.After(b.created) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, sv := range m.SecureValues {
|
||||
if len(toDelete) >= int(maxBatchSize) {
|
||||
break
|
||||
}
|
||||
|
||||
if !sv.active && now.Sub(sv.created) > minAge {
|
||||
toDelete = append(toDelete, sv)
|
||||
}
|
||||
}
|
||||
|
||||
// PERF: The slices are always small
|
||||
m.SecureValues = slices.DeleteFunc(m.SecureValues, func(v1 *ModelSecureValue) bool {
|
||||
return slices.ContainsFunc(toDelete, func(v2 *ModelSecureValue) bool {
|
||||
return v2.UID == v1.UID
|
||||
})
|
||||
})
|
||||
|
||||
return toDelete, nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -143,7 +144,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
|
||||
realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage)
|
||||
require.NoError(t, err)
|
||||
|
||||
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)
|
||||
mockAwsKeeper := NewModelSecretsManager()
|
||||
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper, mockAwsKeeper)
|
||||
|
||||
if setupCfg.KeeperService != nil {
|
||||
keeperService = setupCfg.KeeperService
|
||||
@@ -190,6 +192,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
|
||||
Clock: clock,
|
||||
KeeperService: keeperService,
|
||||
KeeperMetadataStorage: keeperMetadataStorage,
|
||||
ModelSecretsManager: mockAwsKeeper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +215,8 @@ type Sut struct {
|
||||
Clock *FakeClock
|
||||
KeeperService contracts.KeeperService
|
||||
KeeperMetadataStorage contracts.KeeperMetadataStorage
|
||||
// A mock of AWS secrets manager that implements contracts.Keeper
|
||||
ModelSecretsManager *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
type CreateSvConfig struct {
|
||||
@@ -260,16 +265,54 @@ func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv1be
|
||||
return sv, err
|
||||
}
|
||||
|
||||
type keeperServiceWrapper struct {
|
||||
keeper contracts.Keeper
|
||||
type CreateKeeperConfig struct {
|
||||
// The default keeper payload. Mutate it to change which keeper ends up being created
|
||||
Keeper *secretv1beta1.Keeper
|
||||
}
|
||||
|
||||
func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper {
|
||||
return &keeperServiceWrapper{keeper: keeper}
|
||||
func (s *Sut) CreateAWSKeeper(ctx context.Context) (*secretv1beta1.Keeper, error) {
|
||||
return s.CreateKeeper(ctx, func(cfg *CreateKeeperConfig) {
|
||||
cfg.Keeper.Spec = secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Sut) CreateKeeper(ctx context.Context, opts ...func(*CreateKeeperConfig)) (*secretv1beta1.Keeper, error) {
|
||||
cfg := CreateKeeperConfig{
|
||||
Keeper: &secretv1beta1.Keeper{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sv1",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
return s.KeeperMetadataStorage.Create(ctx, cfg.Keeper, "actor-uid")
|
||||
}
|
||||
|
||||
type keeperServiceWrapper struct {
|
||||
sqlKeeper *sqlkeeper.SQLKeeper
|
||||
awsKeeper *ModelAWSSecretsManager
|
||||
}
|
||||
|
||||
func newKeeperServiceWrapper(sqlKeeper *sqlkeeper.SQLKeeper, awsKeeper *ModelAWSSecretsManager) *keeperServiceWrapper {
|
||||
return &keeperServiceWrapper{sqlKeeper: sqlKeeper, awsKeeper: awsKeeper}
|
||||
}
|
||||
|
||||
func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv1beta1.KeeperConfig) (contracts.Keeper, error) {
|
||||
return wrapper.keeper, nil
|
||||
switch cfg.(type) {
|
||||
case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAWSConfig]:
|
||||
return wrapper.awsKeeper, nil
|
||||
default:
|
||||
return wrapper.sqlKeeper, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context {
|
||||
@@ -390,3 +433,113 @@ type NoopMigrationExecutor struct {
|
||||
func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// A mock of AWS secrets manager, used for testing.
|
||||
type ModelAWSSecretsManager struct {
|
||||
secrets map[string]entry
|
||||
alreadyDeleted map[string]bool
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
exposedValueOrRef string
|
||||
externalID string
|
||||
}
|
||||
|
||||
func NewModelSecretsManager() *ModelAWSSecretsManager {
|
||||
return &ModelAWSSecretsManager{
|
||||
secrets: make(map[string]entry),
|
||||
alreadyDeleted: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (externalID contracts.ExternalID, err error) {
|
||||
if exposedValueOrRef == "" {
|
||||
return "", fmt.Errorf("failed to satisfy constraint: Member must have length greater than or equal to 1")
|
||||
}
|
||||
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
if e, ok := m.secrets[versionID]; ok {
|
||||
// Ignore duplicated requests
|
||||
if e.exposedValueOrRef == exposedValueOrRef {
|
||||
return contracts.ExternalID(e.externalID), nil
|
||||
}
|
||||
|
||||
// Tried to create a secret that already exists
|
||||
return "", fmt.Errorf("ResourceExistsException: The operation failed because the secret %+v already exists", versionID)
|
||||
}
|
||||
|
||||
// First time creating the secret
|
||||
entry := entry{
|
||||
exposedValueOrRef: exposedValueOrRef,
|
||||
externalID: "external-id",
|
||||
}
|
||||
m.secrets[versionID] = entry
|
||||
|
||||
return contracts.ExternalID(entry.externalID), nil
|
||||
}
|
||||
|
||||
// Used to simulate the creation of secrets in the 3rd party secret store
|
||||
func (m *ModelAWSSecretsManager) Create(name, value string) {
|
||||
m.secrets[name] = entry{
|
||||
exposedValueOrRef: value,
|
||||
externalID: fmt.Sprintf("external_id_%+v", value),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (exposedValue secretv1beta1.ExposedSecureValue, err error) {
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
|
||||
if m.deleted(versionID) {
|
||||
return "", fmt.Errorf("InvalidRequestException: You can't perform this operation on the secret because it was marked for deletion")
|
||||
}
|
||||
|
||||
entry, ok := m.secrets[versionID]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret")
|
||||
}
|
||||
|
||||
return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil
|
||||
}
|
||||
|
||||
// TODO: this could be namespaced to make it more realistic
|
||||
func (m *ModelAWSSecretsManager) RetrieveReference(ctx context.Context, _ secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) {
|
||||
entry, ok := m.secrets[ref]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret")
|
||||
}
|
||||
return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (err error) {
|
||||
versionID := buildVersionID(namespace, name, version)
|
||||
|
||||
// Deleting a secret that existed at some point is idempotent
|
||||
if m.deleted(versionID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the secret is being deleted for the first time
|
||||
if m.exists(versionID) {
|
||||
m.delete(versionID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) deleted(versionID string) bool {
|
||||
return m.alreadyDeleted[versionID]
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) exists(versionID string) bool {
|
||||
_, ok := m.secrets[versionID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *ModelAWSSecretsManager) delete(versionID string) {
|
||||
m.alreadyDeleted[versionID] = true
|
||||
delete(m.secrets, versionID)
|
||||
}
|
||||
|
||||
func buildVersionID(namespace xkube.Namespace, name string, version int64) string {
|
||||
return fmt.Sprintf("%s/%s/%d", namespace, name, version)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
@@ -9,14 +11,17 @@ import (
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
type keeperValidator struct{}
|
||||
type keeperValidator struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
var _ contracts.KeeperValidator = &keeperValidator{}
|
||||
|
||||
func ProvideKeeperValidator() contracts.KeeperValidator {
|
||||
return &keeperValidator{}
|
||||
func ProvideKeeperValidator(features featuremgmt.FeatureToggles) contracts.KeeperValidator {
|
||||
return &keeperValidator{features: features}
|
||||
}
|
||||
|
||||
func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList {
|
||||
@@ -57,51 +62,110 @@ func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secr
|
||||
}
|
||||
|
||||
if keeper.Spec.Aws != nil {
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyID"), keeper.Spec.Aws.AccessKeyID); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "secretAccessKey"), keeper.Spec.Aws.SecretAccessKey); err != nil {
|
||||
errs = append(errs, err)
|
||||
//nolint
|
||||
if !v.features.IsEnabled(context.Background(), featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper) {
|
||||
errs = append(errs,
|
||||
field.Forbidden(field.NewPath("spec", "aws"),
|
||||
fmt.Sprintf("enable aws keeper feature toggle to create aws keepers: %s", featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper)))
|
||||
} else {
|
||||
errs = append(errs, validateAws(keeper.Spec.Aws)...)
|
||||
}
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure != nil {
|
||||
if keeper.Spec.Azure.KeyVaultName == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure.TenantID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure.ClientID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required"))
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), keeper.Spec.Azure.ClientSecret); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
errs = append(errs, validateAzure(keeper.Spec.Azure)...)
|
||||
}
|
||||
|
||||
if keeper.Spec.Gcp != nil {
|
||||
if keeper.Spec.Gcp.ProjectID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Gcp.CredentialsFile == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required"))
|
||||
}
|
||||
errs = append(errs, validateGcp(keeper.Spec.Gcp)...)
|
||||
}
|
||||
|
||||
if keeper.Spec.HashiCorpVault != nil {
|
||||
if keeper.Spec.HashiCorpVault.Address == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required"))
|
||||
}
|
||||
errs = append(errs, validateHashiCorpVault(keeper.Spec.HashiCorpVault)...)
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), keeper.Spec.HashiCorpVault.Token); err != nil {
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateAws(cfg *secretv1beta1.KeeperAWSConfig) field.ErrorList {
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
if cfg.Region == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "aws", "region"), "region must be present"))
|
||||
}
|
||||
|
||||
switch {
|
||||
case cfg.AccessKey == nil && cfg.AssumeRole == nil:
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "aws"), "one of `accessKey` or `assumeRole` must be present"))
|
||||
|
||||
case cfg.AccessKey != nil && cfg.AssumeRole != nil:
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "aws"), "only one of `accessKey` or `assumeRole` can be present"))
|
||||
|
||||
case cfg.AccessKey != nil:
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKey", "accessKeyID"), cfg.AccessKey.AccessKeyID); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKey", "secretAccessKey"), cfg.AccessKey.SecretAccessKey); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
case cfg.AssumeRole != nil:
|
||||
if cfg.AssumeRole.AssumeRoleArn == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "aws", "assumeRole", "assumeRoleArn"), "arn of the role to assume must be present"))
|
||||
}
|
||||
if cfg.AssumeRole.ExternalID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "aws", "assumeRole", "externalId"), "externalId must be present"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateAzure(cfg *secretv1beta1.KeeperAzureConfig) field.ErrorList {
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
if cfg.KeyVaultName == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required"))
|
||||
}
|
||||
|
||||
if cfg.TenantID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required"))
|
||||
}
|
||||
|
||||
if cfg.ClientID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required"))
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), cfg.ClientSecret); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateGcp(cfg *secretv1beta1.KeeperGCPConfig) field.ErrorList {
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
if cfg.ProjectID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required"))
|
||||
}
|
||||
|
||||
if cfg.CredentialsFile == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateHashiCorpVault(cfg *secretv1beta1.KeeperHashiCorpConfig) field.ErrorList {
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
if cfg.Address == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required"))
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), cfg.Token); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return errs
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func TestValidateKeeper(t *testing.T) {
|
||||
objectMeta := metav1.ObjectMeta{Name: "test", Namespace: "test"}
|
||||
validator := ProvideKeeperValidator()
|
||||
validator := ProvideKeeperValidator(featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper))
|
||||
|
||||
t.Run("when creating a new keeper", func(t *testing.T) {
|
||||
t.Run("the `description` must be present", func(t *testing.T) {
|
||||
@@ -22,9 +23,12 @@ func TestValidateKeeper(t *testing.T) {
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
KmsKeyID: ptr.To("kms-key-id"),
|
||||
Region: "us-east-1",
|
||||
AccessKey: &secretv1beta1.KeeperAWSAccessKey{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
},
|
||||
KmsKeyID: ptr.To("kms-key-id"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -41,30 +45,42 @@ func TestValidateKeeper(t *testing.T) {
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{
|
||||
ValueFromEnv: "some-value",
|
||||
},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "some-value",
|
||||
Region: "us-east-1",
|
||||
AccessKey: &secretv1beta1.KeeperAWSAccessKey{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{
|
||||
ValueFromEnv: "some-value",
|
||||
},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "some-value",
|
||||
},
|
||||
},
|
||||
KmsKeyID: ptr.To("optional"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("aws keeper feature flag must be enabled", func(t *testing.T) {
|
||||
// Validator with feature disabled
|
||||
validator := ProvideKeeperValidator(featuremgmt.WithFeatures())
|
||||
errs := validator.Validate(validKeeperAWS.DeepCopy(), nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws", errs[0].Field)
|
||||
require.Contains(t, errs[0].Detail, "secretsManagementAppPlatformAwsKeeper")
|
||||
})
|
||||
|
||||
t.Run("`accessKeyID` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{}
|
||||
keeper.Spec.Aws.AccessKey.AccessKeyID = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.accessKeyID", errs[0].Field)
|
||||
require.Equal(t, "spec.aws.accessKey.accessKeyID", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{
|
||||
keeper.Spec.Aws.AccessKey.AccessKeyID = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
@@ -72,23 +88,23 @@ func TestValidateKeeper(t *testing.T) {
|
||||
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.accessKeyID", errs[0].Field)
|
||||
require.Equal(t, "spec.aws.accessKey.accessKeyID", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("`secretAccessKey` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{}
|
||||
keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field)
|
||||
require.Equal(t, "spec.aws.accessKey.secretAccessKey", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{
|
||||
keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
@@ -96,7 +112,23 @@ func TestValidateKeeper(t *testing.T) {
|
||||
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field)
|
||||
require.Equal(t, "spec.aws.accessKey.secretAccessKey", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("only one of accessKey or assumeRole can be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
}
|
||||
keeper.Spec.Aws.AssumeRole = &secretv1beta1.KeeperAWSAssumeRole{
|
||||
AssumeRoleArn: "arn",
|
||||
ExternalID: "id",
|
||||
}
|
||||
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws", errs[0].Field)
|
||||
require.Equal(t, "only one of `accessKey` or `assumeRole` can be present", errs[0].Detail)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,3 +167,99 @@ func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, re
|
||||
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
|
||||
}, claims.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
func (c *LegacyAccessClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req claims.BatchCheckRequest) (claims.BatchCheckResponse, error) {
|
||||
ident, ok := id.(identity.Requester)
|
||||
if !ok {
|
||||
return claims.BatchCheckResponse{}, errors.New("expected identity.Requester for legacy access control")
|
||||
}
|
||||
|
||||
results := make(map[string]claims.BatchCheckResult, len(req.Checks))
|
||||
|
||||
// Cache checkers by action to avoid recreating them for each check
|
||||
checkerCache := make(map[string]func(scopes ...string) bool)
|
||||
|
||||
for _, check := range req.Checks {
|
||||
opts, ok := c.opts[check.Resource]
|
||||
if !ok {
|
||||
// For now w fallback to grafana admin if no options are found for resource.
|
||||
if ident.GetIsGrafanaAdmin() {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if verb should be skipped
|
||||
if opts.Unchecked[check.Verb] {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
continue
|
||||
}
|
||||
|
||||
action, ok := opts.Mapping[check.Verb]
|
||||
if !ok {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("missing action for %s %s", check.Verb, check.Resource),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get or create cached checker for this action
|
||||
checker, ok := checkerCache[action]
|
||||
if !ok {
|
||||
checker = Checker(ident, action)
|
||||
checkerCache[action] = checker
|
||||
}
|
||||
|
||||
// Handle list and create verbs (no specific name)
|
||||
// TODO: Should we allow list/create without name in a BatchCheck request?
|
||||
if check.Name == "" {
|
||||
if check.Verb == utils.VerbList || check.Verb == utils.VerbCreate {
|
||||
// For list/create without name, check if user has the action at all
|
||||
// TODO: Is this correct for Create?
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: len(ident.GetPermissions()[action]) > 0,
|
||||
}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("unhandled authorization: %s %s", check.Group, check.Verb),
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check with resolver or direct scope
|
||||
var allowed bool
|
||||
if opts.Resolver != nil {
|
||||
ns, err := claims.ParseNamespace(check.Namespace)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
scopes, err := opts.Resolver.Resolve(ctx, ns, check.Name)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
allowed = checker(scopes...)
|
||||
} else {
|
||||
allowed = checker(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, check.Name))
|
||||
}
|
||||
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: allowed}
|
||||
}
|
||||
|
||||
return claims.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: claims.NoopZookie{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -136,6 +136,220 @@ func TestLegacyAccessClient_Check(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLegacyAccessClient_BatchCheck(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
|
||||
t.Run("should return empty results for empty checks", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res.Results)
|
||||
})
|
||||
|
||||
t.Run("should reject unknown resource for non-admin", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "unknown", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should allow unknown resource for grafana admin", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{IsGrafanaAdmin: true}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "unknown", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should allow unchecked verbs", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Unchecked: map[string]bool{"get": true},
|
||||
})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should return error for missing action mapping", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{}, // Empty mapping
|
||||
})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
assert.Error(t, res.Results["check-1"].Error)
|
||||
})
|
||||
|
||||
t.Run("should allow when user has correct scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should reject when user has wrong scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:2"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should handle list without name", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"list": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:*"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "list", Resource: "dashboards", Name: ""},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should handle multiple checks with mixed results", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"},
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:3"},
|
||||
)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "check-2", Verb: "get", Resource: "dashboards", Name: "2"},
|
||||
{CorrelationID: "check-3", Verb: "get", Resource: "dashboards", Name: "3"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
assert.False(t, res.Results["check-2"].Allowed)
|
||||
assert.True(t, res.Results["check-3"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should use resolver when provided", func(t *testing.T) {
|
||||
resolver := accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) {
|
||||
// Resolve dashboard name to folder scope
|
||||
return []string{"folders:uid:folder-a"}, nil
|
||||
})
|
||||
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
Resolver: resolver,
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "folders:uid:folder-a"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1", Namespace: "default"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should cache checker by action", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read", "update": "dashboards:write"},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:*"},
|
||||
accesscontrol.Permission{Action: "dashboards:write", Scope: "dashboards:uid:1"},
|
||||
)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "read-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "read-2", Verb: "get", Resource: "dashboards", Name: "2"},
|
||||
{CorrelationID: "write-1", Verb: "update", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "write-2", Verb: "update", Resource: "dashboards", Name: "2"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
// Read with wildcard scope should allow all
|
||||
assert.True(t, res.Results["read-1"].Allowed)
|
||||
assert.True(t, res.Results["read-2"].Allowed)
|
||||
// Write only has scope for uid:1
|
||||
assert.True(t, res.Results["write-1"].Allowed)
|
||||
assert.False(t, res.Results["write-2"].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester {
|
||||
pmap := map[string][]string{}
|
||||
for _, p := range permissions {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,6 @@ import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
service AuthzExtentionService {
|
||||
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
|
||||
|
||||
rpc Read(ReadRequest) returns (ReadResponse);
|
||||
rpc Write(WriteRequest) returns (WriteResponse);
|
||||
|
||||
@@ -231,29 +229,6 @@ message WriteRequest {
|
||||
|
||||
message WriteResponse {}
|
||||
|
||||
message BatchCheckRequest {
|
||||
string subject = 1;
|
||||
string namespace = 2;
|
||||
repeated BatchCheckItem items = 3;
|
||||
}
|
||||
|
||||
message BatchCheckItem {
|
||||
string verb = 1;
|
||||
string group = 2;
|
||||
string resource = 3;
|
||||
string name = 4;
|
||||
string subresource = 5;
|
||||
string folder = 6;
|
||||
}
|
||||
|
||||
message BatchCheckResponse {
|
||||
map<string, BatchCheckGroupResource> groups = 1;
|
||||
}
|
||||
|
||||
message BatchCheckGroupResource {
|
||||
map<string, bool> items = 1;
|
||||
}
|
||||
|
||||
message QueryRequest {
|
||||
string namespace = 1;
|
||||
QueryOperation operation = 2;
|
||||
|
||||
@@ -19,18 +19,16 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion8
|
||||
|
||||
const (
|
||||
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
)
|
||||
|
||||
// AuthzExtentionServiceClient is the client API for AuthzExtentionService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AuthzExtentionServiceClient interface {
|
||||
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
|
||||
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
|
||||
Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
|
||||
Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error)
|
||||
@@ -45,16 +43,6 @@ func NewAuthzExtentionServiceClient(cc grpc.ClientConnInterface) AuthzExtentionS
|
||||
return &authzExtentionServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchCheckResponse)
|
||||
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReadResponse)
|
||||
@@ -99,7 +87,6 @@ func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryReques
|
||||
// All implementations should embed UnimplementedAuthzExtentionServiceServer
|
||||
// for forward compatibility
|
||||
type AuthzExtentionServiceServer interface {
|
||||
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
|
||||
Read(context.Context, *ReadRequest) (*ReadResponse, error)
|
||||
Write(context.Context, *WriteRequest) (*WriteResponse, error)
|
||||
Mutate(context.Context, *MutateRequest) (*MutateResponse, error)
|
||||
@@ -110,9 +97,6 @@ type AuthzExtentionServiceServer interface {
|
||||
type UnimplementedAuthzExtentionServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
|
||||
}
|
||||
func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
|
||||
}
|
||||
@@ -137,24 +121,6 @@ func RegisterAuthzExtentionServiceServer(s grpc.ServiceRegistrar, srv AuthzExten
|
||||
s.RegisterService(&AuthzExtentionService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReadRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -234,10 +200,6 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "authz.extention.v1.AuthzExtentionService",
|
||||
HandlerType: (*AuthzExtentionServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "BatchCheck",
|
||||
Handler: _AuthzExtentionService_BatchCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Read",
|
||||
Handler: _AuthzExtentionService_Read_Handler,
|
||||
|
||||
@@ -186,6 +186,150 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv
|
||||
return &authzv1.CheckResponse{Allowed: allowed}, nil
|
||||
}
|
||||
|
||||
// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck
|
||||
// This performs multiple access checks in a single request with optimized batching.
|
||||
// 1. Validates the subject once
|
||||
// 2. Groups checks by (namespace, action) to load permissions once per group
|
||||
// 3. Reuses the folder tree across checks
|
||||
func (s *Service) BatchCheck(ctx context.Context, req *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
checks := req.GetChecks()
|
||||
span.SetAttributes(attribute.Int("check_count", len(checks)))
|
||||
|
||||
ctxLogger := s.logger.FromContext(ctx).New(
|
||||
"subject", req.GetSubject(),
|
||||
"check_count", len(checks),
|
||||
)
|
||||
defer func(start time.Time) {
|
||||
ctxLogger.Debug("BatchCheck execution time", "duration", time.Since(start).Milliseconds())
|
||||
}(time.Now())
|
||||
|
||||
// Early check for auth info - required for namespace validation
|
||||
if _, has := types.AuthInfoFrom(ctx); !has {
|
||||
return nil, status.Error(codes.Internal, "could not get auth info from context")
|
||||
}
|
||||
|
||||
if len(checks) == 0 {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: make(map[string]*authzv1.BatchCheckResult),
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate subject once for all checks
|
||||
userUID, idType, err := s.validateSubject(ctx, req.GetSubject())
|
||||
if err != nil {
|
||||
ctxLogger.Error("invalid subject", "error", err)
|
||||
// Return all checks as denied with the same error
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(checks))
|
||||
for _, item := range checks {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
return &authzv1.BatchCheckResponse{Results: results, Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()}}, nil
|
||||
}
|
||||
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(checks))
|
||||
|
||||
// Group checks by (namespace, action) to batch permission lookups
|
||||
type checkGroup struct {
|
||||
namespace types.NamespaceInfo
|
||||
action string
|
||||
actionSets []string
|
||||
items []*authzv1.BatchCheckItem
|
||||
checkReqs []*checkRequest
|
||||
}
|
||||
groups := make(map[string]*checkGroup)
|
||||
|
||||
// First pass: validate and group checks
|
||||
for _, item := range checks {
|
||||
ns, err := validateNamespace(ctx, item.GetNamespace())
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
action, actionSets, err := s.validateAction(ctx, item.GetGroup(), item.GetResource(), item.GetVerb())
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
// Create the internal check request
|
||||
checkReq := &checkRequest{
|
||||
Namespace: ns,
|
||||
UserUID: userUID,
|
||||
IdentityType: idType,
|
||||
Action: action,
|
||||
ActionSets: actionSets,
|
||||
Group: item.GetGroup(),
|
||||
Resource: item.GetResource(),
|
||||
Verb: item.GetVerb(),
|
||||
Name: item.GetName(),
|
||||
ParentFolder: item.GetFolder(),
|
||||
}
|
||||
|
||||
// Group by namespace + action
|
||||
groupKey := ns.Value + ":" + action
|
||||
if g, ok := groups[groupKey]; ok {
|
||||
g.items = append(g.items, item)
|
||||
g.checkReqs = append(g.checkReqs, checkReq)
|
||||
} else {
|
||||
groups[groupKey] = &checkGroup{
|
||||
namespace: ns,
|
||||
action: action,
|
||||
actionSets: actionSets,
|
||||
items: []*authzv1.BatchCheckItem{item},
|
||||
checkReqs: []*checkRequest{checkReq},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: process each group with shared permissions
|
||||
for _, group := range groups {
|
||||
// Set namespace in context for this group (required by store methods)
|
||||
groupCtx := request.WithNamespace(ctx, group.namespace.Value)
|
||||
|
||||
// Try to get cached permissions first, then fall back to store
|
||||
permissions, err := s.getCachedIdentityPermissions(groupCtx, group.namespace, idType, userUID, group.action)
|
||||
if err != nil {
|
||||
// Cache miss - fetch from store
|
||||
permissions, err = s.getIdentityPermissions(groupCtx, group.namespace, idType, userUID, group.action, group.actionSets)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not get permissions", "namespace", group.namespace.Value, "action", group.action, "error", err)
|
||||
for _, item := range group.items {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check each item in the group using the shared permissions
|
||||
for i, item := range group.items {
|
||||
checkReq := group.checkReqs[i]
|
||||
|
||||
allowed, err := s.checkPermission(groupCtx, permissions, checkReq)
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: allowed}
|
||||
}
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.Int("groups_processed", len(groups)))
|
||||
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1.ListResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.List")
|
||||
defer span.End()
|
||||
|
||||
@@ -1829,6 +1829,613 @@ func TestService_CacheList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_BatchCheck(t *testing.T) {
|
||||
callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"),
|
||||
Audience: []string{"authzservice"},
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{Namespace: "org-12"},
|
||||
})
|
||||
|
||||
t.Run("Require auth info", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := context.Background()
|
||||
_, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "could not get auth info")
|
||||
})
|
||||
|
||||
t.Run("Empty checks returns empty results", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Empty(t, resp.Results)
|
||||
})
|
||||
|
||||
type batchCheckTestCase struct {
|
||||
name string
|
||||
checks []*authzv1.BatchCheckItem
|
||||
permissions []accesscontrol.Permission
|
||||
folders []store.Folder
|
||||
expectedResults map[string]bool
|
||||
expectedErrors map[string]bool // true if error expected for this correlation ID
|
||||
expectGlobalError bool
|
||||
}
|
||||
|
||||
t.Run("Request validation", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should return error for invalid namespace",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for namespace mismatch",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-13",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for unknown group",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "unknown.grafana.app",
|
||||
Resource: "unknown",
|
||||
Verb: "get",
|
||||
Name: "u1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for unknown verb",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "unknown",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
store := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: tc.permissions,
|
||||
}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
if tc.expectedErrors[corrID] {
|
||||
require.NotEmpty(t, result.Error, "expected error for %s", corrID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User permission checks", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should allow user with permission on single resource",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should deny user without permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
},
|
||||
{
|
||||
name: "should handle multiple checks with mixed results",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash3",
|
||||
CorrelationId: "check3",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash3"},
|
||||
},
|
||||
expectedResults: map[string]bool{
|
||||
"check1": true,
|
||||
"check2": false,
|
||||
"check3": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle wildcard permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "*", Kind: "*"}},
|
||||
expectedResults: map[string]bool{"check1": true, "check2": true},
|
||||
},
|
||||
{
|
||||
name: "should handle folder inheritance",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
Folder: "child",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent"},
|
||||
},
|
||||
folders: []store.Folder{
|
||||
{UID: "parent"},
|
||||
{UID: "child", ParentUID: strPtr("parent")},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should handle action sets",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:admin", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should handle checks across different resources",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "folder.grafana.app",
|
||||
Resource: "folders",
|
||||
Verb: "get",
|
||||
Name: "fold1",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "folders:read", Scope: "folders:uid:fold1"},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": true, "check2": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
store := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: tc.permissions,
|
||||
folders: tc.folders,
|
||||
}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.folderStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
if tc.folders != nil {
|
||||
s.folderCache.Set(ctx, folderCacheKey("org-12"), newFolderTree(tc.folders))
|
||||
}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Results, len(tc.expectedResults))
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Anonymous permission checks", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should allow anonymous with permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should deny anonymous without permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
store := &fakeStore{userPermissions: tc.permissions}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "anonymous:0",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Rendering permission checks", func(t *testing.T) {
|
||||
t.Run("should allow rendering with permission", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "render:0",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should deny rendering access to another app resources", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "render:0",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "another.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.False(t, resp.Results["check1"].Allowed)
|
||||
require.NotEmpty(t, resp.Results["check1"].Error)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Invalid subject returns errors for all checks", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
store := &fakeStore{}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "invalid:12",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Results, 2)
|
||||
for _, result := range resp.Results {
|
||||
require.False(t, result.Allowed)
|
||||
require.NotEmpty(t, result.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Grouping optimization", func(t *testing.T) {
|
||||
t.Run("should batch permission lookups for same action", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
fStore := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash2"},
|
||||
},
|
||||
}
|
||||
s.store = fStore
|
||||
s.permissionStore = fStore
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash3",
|
||||
CorrelationId: "check3",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
require.True(t, resp.Results["check2"].Allowed)
|
||||
require.False(t, resp.Results["check3"].Allowed)
|
||||
|
||||
// Verify permissions were fetched only once (1 call for userID + 1 call for basicRole + 1 call for permissions)
|
||||
require.Equal(t, 3, fStore.calls)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_CacheBatchCheck(t *testing.T) {
|
||||
callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"),
|
||||
Audience: []string{"authzservice"},
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{Namespace: "org-12"},
|
||||
})
|
||||
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
|
||||
t.Run("Allow based on cached permissions", func(t *testing.T) {
|
||||
s := setupService()
|
||||
|
||||
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
|
||||
s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": true})
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("Fallback to database on cache miss", func(t *testing.T) {
|
||||
s := setupService()
|
||||
|
||||
// Populate database but not cache
|
||||
fStore := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
}
|
||||
s.store = fStore
|
||||
s.permissionStore = fStore
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
func setupService() *Service {
|
||||
cache := cache.NewLocalCache(cache.Config{Expiry: 5 * time.Minute, CleanupInterval: 5 * time.Minute})
|
||||
logger := log.New("authz-rbac-service")
|
||||
|
||||
@@ -13,7 +13,6 @@ type Client interface {
|
||||
authlib.AccessClient
|
||||
Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error)
|
||||
Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
|
||||
|
||||
Mutate(ctx context.Context, req *authzextv1.MutateRequest) error
|
||||
Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error)
|
||||
|
||||
@@ -68,11 +68,11 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Check")
|
||||
func (c *Client) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
return c.authzext.BatchCheck(ctx, req)
|
||||
return c.authzlibclient.BatchCheck(ctx, id, req)
|
||||
}
|
||||
|
||||
func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error {
|
||||
|
||||
@@ -34,8 +34,11 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
return nil, nil
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
return authlib.BatchCheckResponse{
|
||||
Results: make(map[string]authlib.BatchCheckResult),
|
||||
Zookie: authlib.NoopZookie{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error {
|
||||
|
||||
@@ -132,3 +132,54 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
|
||||
|
||||
return shadowItemChecker, authlib.NoopZookie{}, err
|
||||
}
|
||||
|
||||
func (c *ShadowClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
acResChan := make(chan authlib.BatchCheckResponse, 1)
|
||||
acErrChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
if c.zanzanaClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
zanzanaCtx := context.WithoutCancel(ctx)
|
||||
zanzanaCtxTimeout, cancel := context.WithTimeout(zanzanaCtx, zanzanaTimeout)
|
||||
defer cancel()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("zanzana"))
|
||||
res, err := c.zanzanaClient.BatchCheck(zanzanaCtxTimeout, id, req)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to run zanzana batch check", "error", err)
|
||||
}
|
||||
timer.ObserveDuration()
|
||||
|
||||
acRes := <-acResChan
|
||||
acErr := <-acErrChan
|
||||
|
||||
if acErr == nil {
|
||||
// Compare results for each correlation ID
|
||||
for corrID, acResult := range acRes.Results {
|
||||
zanzanaResult, exists := res.Results[corrID]
|
||||
if !exists {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check missing result", "correlationId", corrID, "user", id.GetUID())
|
||||
continue
|
||||
}
|
||||
if zanzanaResult.Allowed != acResult.Allowed {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check result does not match", "expected", acResult.Allowed, "actual", zanzanaResult.Allowed, "correlationId", corrID, "user", id.GetUID())
|
||||
} else {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("rbac"))
|
||||
res, err := c.accessClient.BatchCheck(ctx, id, req)
|
||||
timer.ObserveDuration()
|
||||
acResChan <- res
|
||||
acErrChan <- err
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
)
|
||||
|
||||
type typeInfo struct {
|
||||
@@ -73,7 +72,7 @@ func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo {
|
||||
return resource
|
||||
}
|
||||
|
||||
func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo {
|
||||
func NewResourceInfoFromBatchItem(i *authzv1.BatchCheckItem) ResourceInfo {
|
||||
typ, relations := getTypeAndRelations(i.GetGroup(), i.GetResource())
|
||||
return newResource(
|
||||
typ,
|
||||
|
||||
@@ -2,97 +2,463 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
// checkKey represents a unique check to be performed
|
||||
type checkKey struct {
|
||||
relation string
|
||||
object string
|
||||
}
|
||||
|
||||
// batchCheckBuilder encapsulates state for building OpenFGA batch checks
|
||||
type batchCheckBuilder struct {
|
||||
subject string
|
||||
contextuals *openfgav1.ContextualTupleKeys
|
||||
checks []*openfgav1.BatchCheckItem
|
||||
checksSeen map[checkKey]bool
|
||||
checkMapping map[string]checkKey
|
||||
counter int
|
||||
}
|
||||
|
||||
func newBatchCheckBuilder(subject string, contextuals *openfgav1.ContextualTupleKeys) *batchCheckBuilder {
|
||||
return &batchCheckBuilder{
|
||||
subject: subject,
|
||||
contextuals: contextuals,
|
||||
checks: make([]*openfgav1.BatchCheckItem, 0),
|
||||
checksSeen: make(map[checkKey]bool),
|
||||
checkMapping: make(map[string]checkKey),
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *batchCheckBuilder) addCheck(relation, object string, context *structpb.Struct) {
|
||||
if object == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := checkKey{relation: relation, object: object}
|
||||
if b.checksSeen[key] {
|
||||
return
|
||||
}
|
||||
b.checksSeen[key] = true
|
||||
|
||||
correlationID := fmt.Sprintf("c%d", b.counter)
|
||||
b.counter++
|
||||
|
||||
b.checks = append(b.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: b.subject,
|
||||
Relation: relation,
|
||||
Object: object,
|
||||
},
|
||||
ContextualTuples: b.contextuals,
|
||||
Context: context,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
b.checkMapping[correlationID] = key
|
||||
}
|
||||
|
||||
// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck
|
||||
// This performs multiple access checks in a single request using OpenFGA's native BatchCheck API.
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "server.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
span.SetAttributes(attribute.Int("check_count", len(r.GetChecks())))
|
||||
|
||||
batchRes := &authzextv1.BatchCheckResponse{
|
||||
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
|
||||
}
|
||||
defer func(t time.Time) {
|
||||
s.metrics.requestDurationSeconds.WithLabelValues("server.BatchCheck", "").Observe(time.Since(t).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
store, err := s.getStoreInfo(ctx, r.GetNamespace())
|
||||
res, err := s.batchCheck(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
s.logger.Error("failed to perform batch check request", "error", err)
|
||||
return nil, fmt.Errorf("failed to perform batch check request: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) batchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
items := r.GetChecks()
|
||||
if len(items) == 0 {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: make(map[string]*authzv1.BatchCheckResult),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Group items by namespace
|
||||
itemsByNamespace := make(map[string][]*authzv1.BatchCheckItem)
|
||||
for _, item := range items {
|
||||
ns := item.GetNamespace()
|
||||
itemsByNamespace[ns] = append(itemsByNamespace[ns], item)
|
||||
}
|
||||
|
||||
// Authorize and get store info for each namespace
|
||||
stores := make(map[string]*storeInfo)
|
||||
for namespace := range itemsByNamespace {
|
||||
if err := authorize(ctx, namespace, s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store, err := s.getStoreInfo(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stores[namespace] = store
|
||||
}
|
||||
|
||||
contextuals, err := s.getContextuals(r.GetSubject())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupResourceAccess := make(map[string]bool)
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(items))
|
||||
subject := r.GetSubject()
|
||||
|
||||
for _, item := range r.GetItems() {
|
||||
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
// Process each namespace separately
|
||||
for namespace, nsItems := range itemsByNamespace {
|
||||
store := stores[namespace]
|
||||
|
||||
// Phase 1: Check GroupResource access (broadest permissions)
|
||||
// Example: user has "get" on "dashboards" group_resource → all dashboards allowed
|
||||
s.runGroupResourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 2: Check folder permission inheritance (can_get, can_create, etc. on parent folder)
|
||||
// Example: user has "can_get" on folder-A → all dashboards in folder-A allowed
|
||||
s.runFolderPermissionPhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 3: Check folder subresource access (folder_get, folder_create, etc.)
|
||||
// Example: user has "folder_get" on folder-A → dashboards in folder-A allowed via subresource
|
||||
s.runFolderSubresourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 4: Check direct resource access
|
||||
// Example: user has "get" directly on dashboard-123
|
||||
s.runDirectResourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
}
|
||||
|
||||
// Mark any remaining unresolved items as denied
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; !resolved {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
}
|
||||
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
func (s *Server) buildResponse(results map[string]*authzv1.BatchCheckResult) *authzv1.BatchCheckResponse {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}
|
||||
}
|
||||
|
||||
// runGroupResourcePhase checks if the user has GroupResource-level access.
|
||||
// This is the broadest permission - if allowed, all items in that group are allowed.
|
||||
func (s *Server) runGroupResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
// Group items by their GroupResource
|
||||
type grInfo struct {
|
||||
relation string
|
||||
grIdent string
|
||||
items []string // correlation IDs
|
||||
}
|
||||
groupedItems := make(map[string]*grInfo) // groupResource -> info
|
||||
|
||||
for _, item := range items {
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
if !common.IsGroupResourceRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource(), item.GetSubresource())
|
||||
if _, ok := batchRes.Groups[groupResource]; !ok {
|
||||
batchRes.Groups[groupResource] = &authzextv1.BatchCheckGroupResource{
|
||||
Items: make(map[string]bool),
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
gr := resource.GroupResource()
|
||||
|
||||
if _, exists := groupedItems[gr]; !exists {
|
||||
groupedItems[gr] = &grInfo{
|
||||
relation: relation,
|
||||
grIdent: resource.GroupResourceIdent(),
|
||||
items: make([]string, 0),
|
||||
}
|
||||
}
|
||||
batchRes.Groups[groupResource].Items[item.GetName()] = res.GetAllowed()
|
||||
groupedItems[gr].items = append(groupedItems[gr].items, item.GetCorrelationId())
|
||||
}
|
||||
|
||||
return batchRes, nil
|
||||
if len(groupedItems) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build batch check for unique GroupResources
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
grCheckMapping := make(map[string]string) // OpenFGA correlationID -> groupResource
|
||||
|
||||
for gr, info := range groupedItems {
|
||||
correlationID := fmt.Sprintf("gr%d", builder.counter)
|
||||
builder.counter++
|
||||
builder.checks = append(builder.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: info.relation,
|
||||
Object: info.grIdent,
|
||||
},
|
||||
ContextualTuples: contextuals,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
grCheckMapping[correlationID] = gr
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to check group resource access", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark all items in allowed GroupResources
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
gr := grCheckMapping[correlationID]
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok && allowed.Allowed {
|
||||
for _, itemCorrelationID := range groupedItems[gr].items {
|
||||
results[itemCorrelationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) batchCheckItem(
|
||||
// runFolderPermissionPhase checks folder permission inheritance (can_get, can_create, etc.).
|
||||
// This applies to folder-based resources like dashboards, panels, etc.
|
||||
func (s *Server) runFolderPermissionPhase(
|
||||
ctx context.Context,
|
||||
r *authzextv1.BatchCheckRequest,
|
||||
item *authzextv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
store *storeInfo,
|
||||
groupResourceAccess map[string]bool,
|
||||
) (*authzv1.CheckResponse, error) {
|
||||
var (
|
||||
relation = common.VerbMapping[item.GetVerb()]
|
||||
resource = common.NewResourceInfoFromBatchItem(item)
|
||||
groupResource = resource.GroupResource()
|
||||
)
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string) // checkKey -> correlation IDs
|
||||
|
||||
allowed, ok := groupResourceAccess[groupResource]
|
||||
if !ok {
|
||||
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
allowed = res.GetAllowed()
|
||||
groupResourceAccess[groupResource] = res.GetAllowed()
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
folderIdent := resource.FolderIdent()
|
||||
|
||||
// Only folder-based generic resources use folder permission inheritance
|
||||
if !resource.IsGeneric() || folderIdent == "" || !isFolderPermissionBasedResource(resource.GroupResource()) {
|
||||
continue
|
||||
}
|
||||
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
rel := common.FolderPermissionRelation(relation)
|
||||
key := checkKey{relation: rel, object: folderIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(rel, folderIdent, resource.Context())
|
||||
}
|
||||
|
||||
if allowed {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if resource.IsGeneric() {
|
||||
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder permission phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
// Mark items allowed by folder permissions
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runFolderSubresourcePhase checks folder subresource access (folder_get, folder_create, etc.).
|
||||
func (s *Server) runFolderSubresourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
var objectIdent string
|
||||
var subresRel string
|
||||
|
||||
if resource.IsGeneric() {
|
||||
// Generic resources: check subresource on folder
|
||||
folderIdent := resource.FolderIdent()
|
||||
if folderIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
if !common.IsSubresourceRelation(subresRel) {
|
||||
continue
|
||||
}
|
||||
objectIdent = folderIdent
|
||||
} else {
|
||||
// Typed resources: check subresource on the resource itself
|
||||
if !resource.HasSubresource() || !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
objectIdent = resource.ResourceIdent()
|
||||
if objectIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: subresRel, object: objectIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(subresRel, objectIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder subresource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runDirectResourcePhase checks direct resource access.
|
||||
func (s *Server) runDirectResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
if !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceIdent := resource.ResourceIdent()
|
||||
if resourceIdent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// For folders, use the computed permission relation
|
||||
checkRelation := relation
|
||||
if resource.Type() == common.TypeFolder {
|
||||
checkRelation = common.FolderPermissionRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: checkRelation, object: resourceIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(checkRelation, resourceIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed direct resource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeOpenFGABatchChecks executes the OpenFGA batch checks in chunks and returns results
|
||||
func (s *Server) executeOpenFGABatchChecks(ctx context.Context, store *storeInfo, builder *batchCheckBuilder) (map[checkKey]bool, error) {
|
||||
const maxChecksPerBatch = 50
|
||||
checkResults := make(map[checkKey]bool)
|
||||
|
||||
for i := 0; i < len(builder.checks); i += maxChecksPerBatch {
|
||||
end := i + maxChecksPerBatch
|
||||
if end > len(builder.checks) {
|
||||
end = len(builder.checks)
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform OpenFGA batch check: %w", err)
|
||||
}
|
||||
|
||||
// Process results
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
key, ok := builder.checkMapping[correlationID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok {
|
||||
checkResults[key] = allowed.Allowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return checkResults, nil
|
||||
}
|
||||
|
||||
@@ -1,193 +1,302 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func testBatchCheck(t *testing.T, server *Server) {
|
||||
newReq := func(subject, verb, group, resource, subresource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
for i, item := range items {
|
||||
items[i] = &authzextv1.BatchCheckItem{
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: item.GetName(),
|
||||
Folder: item.GetFolder(),
|
||||
}
|
||||
// Helper to create a batch check request
|
||||
newReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Checks: items,
|
||||
}
|
||||
}
|
||||
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: namespace,
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
// Helper to create a batch check item with correlation ID (uses default namespace)
|
||||
newItem := func(verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s", group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: namespace,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
// user:2 has group_resource access, so both should be allowed
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "3"},
|
||||
{Name: "3", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "3"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.True(t, res.Groups[groupResource].Items["2"])
|
||||
assert.False(t, res.Groups[groupResource].Items["3"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "3", "2")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "3")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["1"])
|
||||
require.True(t, res.Groups[groupResource].Items["2"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
t.Run("user:8 should be able to read all resource:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.False(t, res.Groups[groupResource].Items["12"])
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "5"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
{Name: "13", Folder: "1"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 4)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
require.False(t, res.Groups[groupResource].Items["13"])
|
||||
require.Len(t, res.Results, 4)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "5", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "13")].Allowed)
|
||||
})
|
||||
|
||||
// Cross-namespace tests
|
||||
t.Run("cross-namespace: items with explicit namespace should be authorized against their own namespace", func(t *testing.T) {
|
||||
// Helper to create item with explicit namespace
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:1 has access to dashboard 1 in folder 1 in "default" namespace
|
||||
// Both items use explicit namespace
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:1",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Item in default namespace (should be allowed - user:1 has access)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
// Another item in default namespace with different correlation ID
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("cross-namespace: items from different namespaces in same batch", func(t *testing.T) {
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:2 has group_resource access in "default" namespace
|
||||
// They should have access in default but not in other-namespace (no tuples there)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:2",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Items in default namespace (should be allowed - user:2 has group_resource access)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
// Items in other-namespace (should be denied - no tuples in other-namespace)
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 4)
|
||||
|
||||
// Default namespace items should be allowed
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
// Other namespace items should be denied (no permissions in that namespace)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("cross-namespace: mixed results across multiple namespaces", func(t *testing.T) {
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:1 has specific access to dashboard 1 in folder 1
|
||||
// user:2 would have broader access, but we're testing user:1
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:1",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Allowed in default namespace
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
// Denied in default namespace (user:1 doesn't have access to dashboard 2)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
// Denied in other-namespace (no tuples)
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -37,14 +36,14 @@ const (
|
||||
// Timeout for List operations
|
||||
listTimeout = 30 * time.Second
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
|
||||
// Resource type constants for benchmarks
|
||||
benchDashboardGroup = "dashboard.grafana.app"
|
||||
benchDashboardResource = "dashboards"
|
||||
benchFolderGroup = "folder.grafana.app"
|
||||
benchFolderResource = "folders"
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
)
|
||||
|
||||
// benchmarkData holds all the generated test data for benchmarks
|
||||
@@ -338,6 +337,14 @@ func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) {
|
||||
}
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
cfg.ZanzanaServer.CacheSettings.CheckCacheLimit = 100000 // Cache check results
|
||||
cfg.ZanzanaServer.CacheSettings.CheckQueryCacheEnabled = true // Cache check subproblems
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheEnabled = true // Cache DB iterators for checks
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheMaxResults = 10000 // Max results per iterator
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorEnabled = true // Share iterators across concurrent checks
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorLimit = 10000 // Max shared iterators
|
||||
|
||||
testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg))
|
||||
|
||||
openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger())
|
||||
@@ -573,58 +580,64 @@ func BenchmarkCheck(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests
|
||||
func BenchmarkBatchCheck(b *testing.B) {
|
||||
srv, data := setupBenchmarkServer(b)
|
||||
ctx := newContextWithNamespace()
|
||||
|
||||
// Helper to create batch check requests
|
||||
newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: benchNamespace,
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
// Helper to create batch check requests using the new authzv1 API
|
||||
newBatchCheckReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Checks: items,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create batch items for resources in folders
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for i := 0; i < batchCheckSize && i < len(resources); i++ {
|
||||
resource := resources[i]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Helper to create batch items for folders at a specific depth
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for _, folder := range folders {
|
||||
if folderDepths[folder] == depth && len(items) < batchCheckSize {
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Fill remaining slots if needed
|
||||
for len(items) < batchCheckSize && len(folders) > 0 {
|
||||
folder := folders[len(items)%len(folders)]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
})
|
||||
}
|
||||
return items
|
||||
@@ -636,6 +649,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with group_resource permission - should have access to everything
|
||||
user := data.users[0]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has group_resource permission (all access)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -643,7 +657,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -651,6 +665,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on shallow folder
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 1, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 1", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -658,7 +673,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -666,6 +681,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on mid-depth folder
|
||||
user := data.users[2*usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 4, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 4", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -673,22 +689,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FolderInheritance/Depth7", func(b *testing.B) {
|
||||
// Check access on deepest folders (worst case for inheritance traversal)
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -696,6 +697,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with direct resource permission
|
||||
user := data.users[4*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has direct resource permission", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -703,22 +705,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("TeamMembership", func(b *testing.B) {
|
||||
// User who is a team member, team has folder permission
|
||||
user := data.users[5*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -726,6 +713,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with no permissions - tests denial path
|
||||
user := data.users[len(data.users)-1]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has NO permissions (denial case)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -733,24 +721,29 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MixedFolders", func(b *testing.B) {
|
||||
// Batch of items across different folder depths
|
||||
user := data.users[usersPerPattern]
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
b.Run("MixedAccess", func(b *testing.B) {
|
||||
// Create items from different folders - user has access to some but not all
|
||||
user := data.users[3*usersPerPattern] // folder-scoped resource permission
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
|
||||
// Mix of accessible and inaccessible resources
|
||||
for i := 0; i < batchCheckSize; i++ {
|
||||
folder := data.folders[i%len(data.folders)]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
})
|
||||
}
|
||||
b.Logf("Testing BatchCheck with %d items, user has mixed access (some allowed, some denied)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -758,9 +751,31 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
// Test BatchCheck at various folder depths
|
||||
for depth := 0; depth <= data.maxDepth; depth++ {
|
||||
depth := depth // capture for closure
|
||||
if len(data.foldersByDepth[depth]) == 0 {
|
||||
continue
|
||||
}
|
||||
b.Run(fmt.Sprintf("ByDepth/Depth%d", depth), func(b *testing.B) {
|
||||
user := fmt.Sprintf("user:depth-%d-access", depth)
|
||||
items := createFolderBatchItems(data.folders, depth, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth %d", len(items), depth)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkList measures the performance of List requests (Compile equivalent)
|
||||
|
||||
17
pkg/services/featuremgmt/ofrep_provider.go
Normal file
17
pkg/services/featuremgmt/ofrep_provider.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
func newOFREPProvider(url string, client *http.Client) (openfeature.FeatureProvider, error) {
|
||||
options := []ofrep.Option{}
|
||||
if client != nil {
|
||||
options = append(options, ofrep.WithClient(client))
|
||||
}
|
||||
|
||||
return ofrep.NewProvider(url, options...), nil
|
||||
}
|
||||
@@ -19,11 +19,11 @@ const (
|
||||
|
||||
// OpenFeatureConfig holds configuration for initializing OpenFeature
|
||||
type OpenFeatureConfig struct {
|
||||
// ProviderType is either "static" or "goff"
|
||||
// ProviderType is either "static", "goff", or "ofrep"
|
||||
ProviderType string
|
||||
// URL is the GOFF service URL (required for GOFF provider)
|
||||
// URL is the GOFF or OFREP service URL (required for GOFF + OFREP providers)
|
||||
URL *url.URL
|
||||
// HTTPClient is a pre-configured HTTP client (optional, used for GOFF provider)
|
||||
// HTTPClient is a pre-configured HTTP client (optional, used for GOFF + OFREP providers)
|
||||
HTTPClient *http.Client
|
||||
// StaticFlags are the feature flags to use with static provider
|
||||
StaticFlags map[string]bool
|
||||
@@ -35,9 +35,9 @@ type OpenFeatureConfig struct {
|
||||
|
||||
// InitOpenFeature initializes OpenFeature with the provided configuration
|
||||
func InitOpenFeature(config OpenFeatureConfig) error {
|
||||
// For GOFF provider, ensure we have a URL
|
||||
if config.ProviderType == setting.GOFFProviderType && (config.URL == nil || config.URL.String() == "") {
|
||||
return fmt.Errorf("URL is required for GOFF provider")
|
||||
// For GOFF + OFREP providers, ensure we have a URL
|
||||
if (config.ProviderType == setting.GOFFProviderType || config.ProviderType == setting.OFREPProviderType) && (config.URL == nil || config.URL.String() == "") {
|
||||
return fmt.Errorf("URL is required for GOFF + OFREP providers")
|
||||
}
|
||||
|
||||
p, err := createProvider(config.ProviderType, config.URL, config.StaticFlags, config.HTTPClient)
|
||||
@@ -66,13 +66,17 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
|
||||
}
|
||||
|
||||
var httpcli *http.Client
|
||||
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType {
|
||||
m, err := clientauthmiddleware.NewTokenExchangeMiddleware(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token exchange middleware: %w", err)
|
||||
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType || cfg.OpenFeature.ProviderType == setting.OFREPProviderType {
|
||||
var m *clientauthmiddleware.TokenExchangeMiddleware
|
||||
|
||||
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType {
|
||||
m, err = clientauthmiddleware.NewTokenExchangeMiddleware(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token exchange middleware: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
httpcli, err = goffHTTPClient(m)
|
||||
httpcli, err = createHTTPClient(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,28 +103,35 @@ func createProvider(
|
||||
staticFlags map[string]bool,
|
||||
httpClient *http.Client,
|
||||
) (openfeature.FeatureProvider, error) {
|
||||
if providerType != setting.GOFFProviderType {
|
||||
return newStaticProvider(staticFlags)
|
||||
if providerType == setting.GOFFProviderType || providerType == setting.OFREPProviderType {
|
||||
if u == nil || u.String() == "" {
|
||||
return nil, fmt.Errorf("feature provider url is required for GOFFProviderType + OFREPProviderType")
|
||||
}
|
||||
|
||||
if providerType == setting.GOFFProviderType {
|
||||
return newGOFFProvider(u.String(), httpClient)
|
||||
}
|
||||
|
||||
if providerType == setting.OFREPProviderType {
|
||||
return newOFREPProvider(u.String(), httpClient)
|
||||
}
|
||||
}
|
||||
|
||||
if u == nil || u.String() == "" {
|
||||
return nil, fmt.Errorf("feature provider url is required for GOFFProviderType")
|
||||
}
|
||||
|
||||
return newGOFFProvider(u.String(), httpClient)
|
||||
return newStaticProvider(staticFlags)
|
||||
}
|
||||
|
||||
func goffHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
|
||||
httpcli, err := sdkhttpclient.NewProvider().New(sdkhttpclient.Options{
|
||||
func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
|
||||
options := sdkhttpclient.Options{
|
||||
TLS: &sdkhttpclient.TLSOptions{InsecureSkipVerify: true},
|
||||
Timeouts: &sdkhttpclient.TimeoutOptions{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
Middlewares: []sdkhttpclient.Middleware{
|
||||
m.New([]string{featuresProviderAudience}),
|
||||
},
|
||||
})
|
||||
}
|
||||
if m != nil {
|
||||
options.Middlewares = append(options.Middlewares, m.New([]string{featuresProviderAudience}))
|
||||
}
|
||||
|
||||
httpcli, err := sdkhttpclient.NewProvider().New(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create http client for openfeature: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
|
||||
ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -60,6 +61,15 @@ func TestCreateProvider(t *testing.T) {
|
||||
expectedProvider: setting.GOFFProviderType,
|
||||
failSigning: true,
|
||||
},
|
||||
{
|
||||
name: "ofrep provider",
|
||||
cfg: setting.OpenFeatureSettings{
|
||||
ProviderType: setting.OFREPProviderType,
|
||||
URL: u,
|
||||
TargetingKey: "grafana",
|
||||
},
|
||||
expectedProvider: setting.OFREPProviderType,
|
||||
},
|
||||
{
|
||||
name: "invalid provider",
|
||||
cfg: setting.OpenFeatureSettings{
|
||||
@@ -96,20 +106,24 @@ func TestCreateProvider(t *testing.T) {
|
||||
}
|
||||
|
||||
tokenExchangeMiddleware := middleware.TestingTokenExchangeMiddleware(tokenExchangeClient)
|
||||
goffClient, err := goffHTTPClient(tokenExchangeMiddleware)
|
||||
httpClient, err := createHTTPClient(tokenExchangeMiddleware)
|
||||
require.NoError(t, err, "failed to create goff http client")
|
||||
provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, goffClient)
|
||||
provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, httpClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = openfeature.SetProviderAndWait(provider)
|
||||
require.NoError(t, err, "failed to set provider")
|
||||
|
||||
if tc.expectedProvider == setting.GOFFProviderType {
|
||||
switch tc.expectedProvider {
|
||||
case setting.GOFFProviderType:
|
||||
_, ok := provider.(*gofeatureflag.Provider)
|
||||
assert.True(t, ok, "expected provider to be of type goff.Provider")
|
||||
|
||||
testGoFFProvider(t, tc.failSigning)
|
||||
} else {
|
||||
case setting.OFREPProviderType:
|
||||
_, ok := provider.(*ofrep.Provider)
|
||||
assert.True(t, ok, "expected provider to be of type ofrep.Provider")
|
||||
default:
|
||||
_, ok := provider.(*inMemoryBulkProvider)
|
||||
assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider")
|
||||
}
|
||||
|
||||
@@ -2082,6 +2082,14 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDataProSquad,
|
||||
},
|
||||
{
|
||||
Name: "secretsManagementAppPlatformAwsKeeper",
|
||||
Description: "Enables the creation of keepers that manage secrets stored on AWS secrets manager",
|
||||
Stage: FeatureStageExperimental,
|
||||
HideFromDocs: true,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaOperatorExperienceSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -282,3 +282,4 @@ kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,fals
|
||||
useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
|
||||
smoothingTransformation,experimental,@grafana/datapro,false,false,true
|
||||
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -781,4 +781,8 @@ const (
|
||||
// FlagKubernetesAlertingHistorian
|
||||
// Adds support for Kubernetes alerting historian APIs
|
||||
FlagKubernetesAlertingHistorian = "kubernetesAlertingHistorian"
|
||||
|
||||
// FlagSecretsManagementAppPlatformAwsKeeper
|
||||
// Enables the creation of keepers that manage secrets stored on AWS secrets manager
|
||||
FlagSecretsManagementAppPlatformAwsKeeper = "secretsManagementAppPlatformAwsKeeper"
|
||||
)
|
||||
|
||||
16
pkg/services/featuremgmt/toggles_gen.json
generated
16
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -3254,6 +3254,22 @@
|
||||
"codeowner": "@grafana/grafana-operator-experience-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "secretsManagementAppPlatformAwsKeeper",
|
||||
"resourceVersion": "1767706420889",
|
||||
"creationTimestamp": "2026-01-06T12:55:50Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-06 13:33:40.889447 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the creation of keepers that manage secrets stored on AWS secrets manager",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-operator-experience-squad",
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "secretsManagementAppPlatformUI",
|
||||
|
||||
@@ -73,21 +73,21 @@ func TestBuildLabelMatcherJSON(t *testing.T) {
|
||||
name: "MySQL MatchEqual with non-empty value",
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: "alerting"},
|
||||
wantSQL: "JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ?",
|
||||
wantSQL: `JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) = ?`,
|
||||
wantArgs: []any{"team", "alerting"},
|
||||
},
|
||||
{
|
||||
name: "MySQL MatchEqual with empty value",
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: ""},
|
||||
wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ? OR JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NULL)",
|
||||
wantSQL: `(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) = ? OR JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"')) IS NULL)`,
|
||||
wantArgs: []any{"team", "", "team"},
|
||||
},
|
||||
{
|
||||
name: "MySQL MatchNotEqual",
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
matcher: &labels.Matcher{Type: labels.MatchNotEqual, Name: "team", Value: "alerting"},
|
||||
wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) != ?)",
|
||||
wantSQL: `(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) != ?)`,
|
||||
wantArgs: []any{"team", "team", "alerting"},
|
||||
},
|
||||
{
|
||||
@@ -149,7 +149,7 @@ func TestBuildLabelKeyExistsCondition(t *testing.T) {
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
column: "labels",
|
||||
key: "__grafana_origin",
|
||||
wantSQL: "JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NOT NULL",
|
||||
wantSQL: `JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"')) IS NOT NULL`,
|
||||
wantArgs: []any{"__grafana_origin"},
|
||||
},
|
||||
{
|
||||
@@ -194,7 +194,7 @@ func TestBuildLabelKeyMissingCondition(t *testing.T) {
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
column: "labels",
|
||||
key: "__grafana_origin",
|
||||
wantSQL: "JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NULL",
|
||||
wantSQL: `JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"')) IS NULL`,
|
||||
wantArgs: []any{"__grafana_origin"},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2454,7 +2454,7 @@ func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
ruleGen.WithLabels(map[string]string{"glob": "*[?]"}),
|
||||
ruleGen.WithTitle("rule_glob")))
|
||||
ruleSpecialChars := createRule(t, store, ruleGen.With(
|
||||
ruleGen.WithLabels(map[string]string{"json": "line1\nline2\\end\"quote"}),
|
||||
ruleGen.WithLabels(map[string]string{"label-with-hyphen": "line1\nline2\\end\"quote"}),
|
||||
ruleGen.WithTitle("rule_special_chars")))
|
||||
ruleEmpty := createRule(t, store, ruleGen.With(
|
||||
ruleGen.WithLabels(map[string]string{"empty": ""}),
|
||||
@@ -2531,7 +2531,7 @@ func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
name: "JSON escape characters are handled correctly",
|
||||
labelMatchers: labels.Matchers{
|
||||
func() *labels.Matcher {
|
||||
m, _ := labels.NewMatcher(labels.MatchEqual, "json", "line1\nline2\\end\"quote")
|
||||
m, _ := labels.NewMatcher(labels.MatchEqual, "label-with-hyphen", "line1\nline2\\end\"quote")
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func jsonEquals(dialect migrator.Dialect, column, key, value string) (string, []any) {
|
||||
switch dialect.DriverName() {
|
||||
case migrator.MySQL:
|
||||
return fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?))) = ?", column), []any{key, value}
|
||||
return fmt.Sprintf(`JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$."', ?, '"'))) = ?`, column), []any{key, value}
|
||||
case migrator.Postgres:
|
||||
return fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?) = ?", column), []any{key, value}
|
||||
default:
|
||||
@@ -25,7 +25,7 @@ func jsonNotEquals(dialect migrator.Dialect, column, key, value string) (string,
|
||||
var jx string
|
||||
switch dialect.DriverName() {
|
||||
case migrator.MySQL:
|
||||
jx = fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?)))", column)
|
||||
jx = fmt.Sprintf(`JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$."', ?, '"')))`, column)
|
||||
case migrator.Postgres:
|
||||
jx = fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?)", column)
|
||||
default:
|
||||
@@ -49,7 +49,7 @@ func jsonKeyCondition(dialect migrator.Dialect, column, key string, exists bool)
|
||||
}
|
||||
switch dialect.DriverName() {
|
||||
case migrator.MySQL:
|
||||
return fmt.Sprintf("JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?)) %s", column, nullCheck), []any{key}, nil
|
||||
return fmt.Sprintf(`JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$."', ?, '"')) %s`, column, nullCheck), []any{key}, nil
|
||||
case migrator.Postgres:
|
||||
return fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?) %s", column, nullCheck), []any{key}, nil
|
||||
default:
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestJsonEquals(t *testing.T) {
|
||||
column: "labels",
|
||||
key: "team",
|
||||
value: "alerting",
|
||||
wantSQL: "JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ?",
|
||||
wantSQL: `JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) = ?`,
|
||||
wantArgs: []any{"team", "alerting"},
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@ func TestJsonNotEquals(t *testing.T) {
|
||||
column: "labels",
|
||||
key: "team",
|
||||
value: "alerting",
|
||||
wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) != ?)",
|
||||
wantSQL: `(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"'))) != ?)`,
|
||||
wantArgs: []any{"team", "team", "alerting"},
|
||||
},
|
||||
{
|
||||
@@ -99,7 +99,7 @@ func TestJsonKeyMissing(t *testing.T) {
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
column: "labels",
|
||||
key: "team",
|
||||
wantSQL: "JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NULL",
|
||||
wantSQL: `JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"')) IS NULL`,
|
||||
wantArgs: []any{"team"},
|
||||
},
|
||||
{
|
||||
@@ -136,7 +136,7 @@ func TestJsonKeyExists(t *testing.T) {
|
||||
dialect: migrator.NewMysqlDialect(),
|
||||
column: "labels",
|
||||
key: "__grafana_origin",
|
||||
wantSQL: "JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NOT NULL",
|
||||
wantSQL: `JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$."', ?, '"')) IS NOT NULL`,
|
||||
wantArgs: []any{"__grafana_origin"},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -55,6 +55,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
||||
_ "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginslog" // Initialize plugin logger
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
|
||||
59
pkg/services/pluginsintegration/pluginslog/pluginslog.go
Normal file
59
pkg/services/pluginsintegration/pluginslog/pluginslog.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package pluginslog
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
pluginslog "github.com/grafana/grafana/pkg/plugins/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register Grafana's logger implementation for pkg/plugins
|
||||
pluginslog.SetLoggerFactory(func(name string) pluginslog.Logger {
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: log.New(name),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type grafanaInfraLogWrapper struct {
|
||||
l *log.ConcreteLogger
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) New(ctx ...any) pluginslog.Logger {
|
||||
if len(ctx) == 0 {
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: d.l.New(),
|
||||
}
|
||||
}
|
||||
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: d.l.New(ctx...),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Debug(msg string, ctx ...any) {
|
||||
d.l.Debug(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Info(msg string, ctx ...any) {
|
||||
d.l.Info(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Warn(msg string, ctx ...any) {
|
||||
d.l.Warn(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) Error(msg string, ctx ...any) {
|
||||
d.l.Error(msg, ctx...)
|
||||
}
|
||||
|
||||
func (d *grafanaInfraLogWrapper) FromContext(ctx context.Context) pluginslog.Logger {
|
||||
concreteInfraLogger, ok := d.l.FromContext(ctx).(*log.ConcreteLogger)
|
||||
if !ok {
|
||||
return d.New()
|
||||
}
|
||||
return &grafanaInfraLogWrapper{
|
||||
l: concreteInfraLogger,
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
const (
|
||||
StaticProviderType = "static"
|
||||
GOFFProviderType = "goff"
|
||||
OFREPProviderType = "ofrep"
|
||||
)
|
||||
|
||||
type OpenFeatureSettings struct {
|
||||
@@ -33,7 +34,7 @@ func (cfg *Cfg) readOpenFeatureSettings() error {
|
||||
|
||||
cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(defaultTargetingKey)
|
||||
|
||||
if strURL != "" && cfg.OpenFeature.ProviderType == GOFFProviderType {
|
||||
if strURL != "" && (cfg.OpenFeature.ProviderType == GOFFProviderType || cfg.OpenFeature.ProviderType == OFREPProviderType) {
|
||||
u, err := url.Parse(strURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid feature provider url: %w", err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user