Compare commits
9 Commits
authnwithf
...
colin-stua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f140eef4b | ||
|
|
29b04bd2ed | ||
|
|
c0fe27406b | ||
|
|
9409af6f1c | ||
|
|
e1c60e0a83 | ||
|
|
1f88aeb91f | ||
|
|
901360dca4 | ||
|
|
2cf485f6bf | ||
|
|
8ff88036e7 |
@@ -69,12 +69,12 @@ require (
|
||||
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.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // 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/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.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
|
||||
@@ -162,14 +162,14 @@ require (
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
|
||||
@@ -177,38 +177,38 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
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=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
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/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
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.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/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
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/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
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/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
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/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/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
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=
|
||||
@@ -637,8 +637,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
@@ -655,8 +655,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
|
||||
@@ -108,22 +108,22 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.55.7 // 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/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // 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.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/ini v1.8.4 // 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
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
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/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.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
|
||||
@@ -229,7 +229,7 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
|
||||
@@ -242,7 +242,7 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
|
||||
@@ -242,20 +242,20 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
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=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
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/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
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.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/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4=
|
||||
@@ -264,12 +264,12 @@ github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFie
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54=
|
||||
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/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
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/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo=
|
||||
@@ -282,12 +282,12 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZ
|
||||
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/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
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=
|
||||
@@ -853,8 +853,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI=
|
||||
@@ -891,8 +891,8 @@ github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae h1:35W3Wjp
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec h1:wnzJov9RhSHGaTYGzTygL4qq986fLen8xSqnQgaMd28=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec/go.mod h1:j1IY7J2rUz7TcTjFVVx6HCpyTlYOJPtXuGRZ7sI+vSo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
|
||||
@@ -31,12 +31,12 @@ require (
|
||||
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.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // 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/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.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
|
||||
@@ -97,14 +97,14 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
|
||||
@@ -30,18 +30,18 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
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/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
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/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
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=
|
||||
@@ -229,8 +229,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
@@ -243,8 +243,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
|
||||
|
||||
@@ -428,12 +428,25 @@ Or using a Kubernetes format, for example `kubernetes-dashboard.json`:
|
||||
|
||||
You _must_ use the Kubernetes resource format to provision dashboards v2 / dynamic dashboards.
|
||||
|
||||
It later polls that path every `updateIntervalSeconds` for updates to the dashboard files and updates its database.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Grafana installs dashboards at the root level if you don't set the `folder` field.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### Detect updates to provisioned dashboards files
|
||||
|
||||
After Grafana provisions your dashboards, it checks the filesystem for changes and updates dashboards as needed.
|
||||
|
||||
The mechanism Grafana uses to do this depends on your `updateIntervalSeconds` value:
|
||||
|
||||
- **More than 10 seconds**: Grafana polls the path at that interval.
|
||||
- **10 seconds or less**: Grafana watches the filesystem for changes and updates dashboards when it detects them.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
When `updateIntervalSeconds` is 10 or less, Grafana relies on filesystem watch events to detect changes.
|
||||
Depending on your filesystem and how you mount or sync dashboard files (for example, Docker bind mounts or some network filesystems), those events might not reach Grafana.
|
||||
To work around this, set `updateIntervalSeconds` to more than 10 to force polling, or update your setup so filesystem watch events are propagated.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### Make changes to a provisioned dashboard
|
||||
|
||||
You can make changes to a provisioned dashboard in the Grafana UI but its not possible to automatically save the changes back to the provisioning source.
|
||||
|
||||
22
go.mod
22
go.mod
@@ -100,7 +100,7 @@ require (
|
||||
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
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources
|
||||
@@ -342,23 +342,23 @@ 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-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/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // 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.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/ini v1.8.4 // 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
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
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/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
@@ -456,7 +456,6 @@ require (
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
github.com/hashicorp/consul/api v1.31.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@@ -683,6 +682,7 @@ require (
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
|
||||
44
go.sum
44
go.sum
@@ -854,20 +854,20 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
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=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
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/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
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.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/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4=
|
||||
@@ -876,12 +876,12 @@ github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFie
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54=
|
||||
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/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
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/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo=
|
||||
@@ -894,12 +894,12 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZ
|
||||
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/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
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=
|
||||
@@ -1651,8 +1651,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI=
|
||||
@@ -1691,8 +1691,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 h1:SDGrP81Vcd102L3UJEryRd1eestRw73wt+b8vnVEFe0=
|
||||
github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56/go.mod h1:S4+611dxnKt8z/ulbvaJzcgSHsuhjVc1QHNTcr1R7Fw=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec h1:wnzJov9RhSHGaTYGzTygL4qq986fLen8xSqnQgaMd28=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec/go.mod h1:j1IY7J2rUz7TcTjFVVx6HCpyTlYOJPtXuGRZ7sI+vSo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
|
||||
18
go.work.sum
18
go.work.sum
@@ -424,23 +424,30 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTCl
|
||||
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 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
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/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87 h1:oDPArGgCrG/4aTi86ij3S2PB59XXkTSKYVNQlmqRHXQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87/go.mod h1:ZeQC4gVarhdcWeM1c90DyBLaBCNhEeAbKUXwVI/byvw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY=
|
||||
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.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/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||
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/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||
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.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=
|
||||
@@ -448,11 +455,13 @@ github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk7
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
|
||||
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/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s=
|
||||
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/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0 h1:JPXkrQk5OS/+Q81fKH97Ll/Vmmy0p9vwHhxw+V+tVjg=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0/go.mod h1:dJngkoVMrq0K7QvRkdRZYM4NUp6cdWa2GBdpm8zoY8U=
|
||||
@@ -486,10 +495,13 @@ github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1 h1:OwMzNDe5VVTXD4kGmeK/FtqAITiV
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1/go.mod h1:IyVabkWrs8SNdOEZLyFFcW9bUltV4G6OQS0s6H20PHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8=
|
||||
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.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM=
|
||||
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.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
@@ -934,6 +946,7 @@ github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9
|
||||
github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE=
|
||||
github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8=
|
||||
github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw=
|
||||
@@ -981,6 +994,7 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
@@ -1840,6 +1854,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.62.0/go.mod h1:WfEApdZDMlLUAev/0QQpr8EJ/z0VWDKYZ5tF5RH5T1U=
|
||||
@@ -1946,6 +1961,7 @@ 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/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=
|
||||
@@ -1959,6 +1975,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-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
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=
|
||||
@@ -2052,6 +2069,7 @@ golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
|
||||
@@ -699,10 +699,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
playlistsReconciler?: boolean;
|
||||
/**
|
||||
* Enable passwordless login via magic link authentication
|
||||
*/
|
||||
passwordlessMagicLinkAuthentication?: boolean;
|
||||
/**
|
||||
* Display Related Logs in Grafana Metrics Drilldown
|
||||
*/
|
||||
exploreMetricsRelatedLogs?: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useId, memo, HTMLAttributes, ReactNode } from 'react';
|
||||
import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react';
|
||||
|
||||
import { FieldDisplay } from '@grafana/data';
|
||||
|
||||
@@ -50,14 +50,13 @@ export const RadialArcPath = memo(
|
||||
}: RadialArcPathProps) => {
|
||||
const id = useId();
|
||||
|
||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: '100%', height: '100%' };
|
||||
if ('color' in rest) {
|
||||
bgDivStyle.backgroundColor = rest.color;
|
||||
} else {
|
||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||
}
|
||||
const isGradient = 'gradient' in rest;
|
||||
|
||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
||||
const { vizWidth, vizHeight, radius, centerX, centerY, barWidth } = dimensions;
|
||||
const pad = Math.ceil(Math.max(2, barWidth / 2)); // pad to cover stroke caps and glow in Safari
|
||||
const boxX = Math.round(centerX - radius - barWidth - pad);
|
||||
const boxY = Math.round(centerY - radius - barWidth - pad);
|
||||
const boxSize = Math.round((radius + barWidth) * 2 + pad * 2);
|
||||
|
||||
const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars);
|
||||
|
||||
@@ -72,9 +71,14 @@ export const RadialArcPath = memo(
|
||||
const dotRadius =
|
||||
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
||||
|
||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
|
||||
|
||||
const pathProps: SVGProps<SVGPathElement> = {};
|
||||
let barEndcapColors: [string, string] | undefined;
|
||||
let endpointMarks: ReactNode = null;
|
||||
if ('gradient' in rest) {
|
||||
if (isGradient) {
|
||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||
|
||||
if (endpointMarker && (rest.gradient?.length ?? 0) > 0) {
|
||||
switch (endpointMarker) {
|
||||
case 'point':
|
||||
@@ -115,25 +119,39 @@ export const RadialArcPath = memo(
|
||||
if (barEndcaps) {
|
||||
barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent);
|
||||
}
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = 'white';
|
||||
} else {
|
||||
bgDivStyle.backgroundColor = rest.color;
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = rest.color;
|
||||
}
|
||||
|
||||
const pathEl = (
|
||||
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* FIXME: optimize this by only using clippath + foreign obj for gradients */}
|
||||
<clipPath id={id}>
|
||||
<path d={path} />
|
||||
</clipPath>
|
||||
{isGradient && (
|
||||
<defs>
|
||||
<mask id={id} maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
|
||||
<rect x={boxX} y={boxY} width={boxSize} height={boxSize} fill="black" />
|
||||
{pathEl}
|
||||
</mask>
|
||||
</defs>
|
||||
)}
|
||||
|
||||
<g filter={glowFilter}>
|
||||
<foreignObject
|
||||
x={centerX - radius - barWidth}
|
||||
y={centerY - radius - barWidth}
|
||||
width={(radius + barWidth) * 2}
|
||||
height={(radius + barWidth) * 2}
|
||||
clipPath={`url(#${id})`}
|
||||
>
|
||||
<div style={bgDivStyle} />
|
||||
</foreignObject>
|
||||
{isGradient ? (
|
||||
<foreignObject x={0} y={0} width={vizWidth} height={vizHeight} mask={`url(#${id})`}>
|
||||
<div style={bgDivStyle} />
|
||||
</foreignObject>
|
||||
) : (
|
||||
pathEl
|
||||
)}
|
||||
{barEndcapColors?.[0] && <circle cx={xStart} cy={yStart} r={barWidth / 2} fill={barEndcapColors[0]} />}
|
||||
{barEndcapColors?.[1] && (
|
||||
<circle cx={xEnd} cy={yEnd} r={barWidth / 2} fill={barEndcapColors[1]} opacity={0.5} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useId } from 'react';
|
||||
import { useId, ReactNode } from 'react';
|
||||
|
||||
import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -107,14 +107,14 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
const startAngle = shape === 'gauge' ? 250 : 0;
|
||||
const endAngle = shape === 'gauge' ? 110 : 360;
|
||||
|
||||
const defs: React.ReactNode[] = [];
|
||||
const graphics: React.ReactNode[] = [];
|
||||
let sparklineElement: React.ReactNode | null = null;
|
||||
const defs: ReactNode[] = [];
|
||||
const graphics: ReactNode[] = [];
|
||||
let sparklineElement: ReactNode | null = null;
|
||||
|
||||
for (let barIndex = 0; barIndex < values.length; barIndex++) {
|
||||
const displayValue = values[barIndex];
|
||||
const { angle, angleRange } = getValueAngleForValue(displayValue, startAngle, endAngle);
|
||||
const gradientStops = buildGradientColors(gradient, theme, displayValue);
|
||||
const gradientStops = gradient ? buildGradientColors(theme, displayValue) : undefined;
|
||||
const color = displayValue.display.color ?? FALLBACK_COLOR;
|
||||
const dimensions = calculateDimensions(
|
||||
width,
|
||||
@@ -131,7 +131,9 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
// FIXME: I want to move the ids for these filters into a context which the children
|
||||
// can reference via a hook, rather than passing them down as props
|
||||
const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`;
|
||||
const spotlightGradientRef = endpointMarker === 'glow' ? `url(#${spotlightGradientId})` : undefined;
|
||||
const glowFilterId = `glow-${gaugeId}`;
|
||||
const glowFilterRef = glowBar ? `url(#${glowFilterId})` : undefined;
|
||||
|
||||
if (endpointMarker === 'glow') {
|
||||
defs.push(
|
||||
@@ -154,7 +156,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
fieldDisplay={displayValue}
|
||||
angleRange={angleRange}
|
||||
startAngle={startAngle}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
segmentCount={segmentCount}
|
||||
segmentSpacing={segmentSpacing}
|
||||
shape={shape}
|
||||
@@ -170,8 +172,8 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
angleRange={angleRange}
|
||||
startAngle={startAngle}
|
||||
roundedBars={roundedBars}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
endpointMarkerGlowFilter={`url(#${spotlightGradientId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
endpointMarkerGlowFilter={spotlightGradientRef}
|
||||
shape={shape}
|
||||
gradient={gradientStops}
|
||||
fieldDisplay={displayValue}
|
||||
@@ -183,7 +185,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
// These elements are only added for first value / bar
|
||||
if (barIndex === 0) {
|
||||
if (glowBar) {
|
||||
defs.push(<GlowGradient key="glow-filter" id={glowFilterId} barWidth={dimensions.barWidth} />);
|
||||
defs.push(<GlowGradient key={glowFilterId} id={glowFilterId} barWidth={dimensions.barWidth} />);
|
||||
}
|
||||
|
||||
if (glowCenter) {
|
||||
@@ -234,7 +236,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
endAngle={endAngle}
|
||||
angleRange={angleRange}
|
||||
roundedBars={roundedBars}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
shape={shape}
|
||||
gradient={gradientStops}
|
||||
/>
|
||||
@@ -260,7 +262,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
const body = (
|
||||
<>
|
||||
<svg width={width} height={height} role="img" aria-label={t('gauge.category-gauge', 'Gauge')}>
|
||||
<defs>{defs}</defs>
|
||||
{defs.length > 0 && <defs>{defs}</defs>}
|
||||
{graphics}
|
||||
</svg>
|
||||
{sparklineElement}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import {
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { calculateFontSize } from '../../utils/measureText';
|
||||
|
||||
import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types';
|
||||
@@ -50,7 +48,6 @@ export const RadialText = memo(
|
||||
valueManualFontSize,
|
||||
nameManualFontSize,
|
||||
}: RadialTextProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { centerX, centerY, radius, barWidth } = dimensions;
|
||||
|
||||
if (textMode === 'none') {
|
||||
@@ -106,10 +103,9 @@ export const RadialText = memo(
|
||||
const valueY = showName ? centerY - nameHeight * (1 - VALUE_SPACE_PERCENTAGE) : centerY;
|
||||
const nameY = showValue ? valueY + valueHeight * VALUE_SPACE_PERCENTAGE : centerY;
|
||||
const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary;
|
||||
const suffixShift = (valueFontSize - unitFontSize * LINE_HEIGHT_FACTOR) / 2;
|
||||
|
||||
// adjust the text up on gauges and when sparklines are present
|
||||
let yOffset = 0;
|
||||
let yOffset = valueFontSize / 4;
|
||||
if (shape === 'gauge') {
|
||||
// we render from the center of the gauge, so move up by half of half of the total height
|
||||
yOffset -= (valueHeight + nameHeight) / 4;
|
||||
@@ -126,15 +122,12 @@ export const RadialText = memo(
|
||||
y={valueY}
|
||||
fontSize={valueFontSize}
|
||||
fill={theme.colors.text.primary}
|
||||
className={styles.text}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
dominantBaseline="text-bottom"
|
||||
>
|
||||
<tspan fontSize={unitFontSize}>{displayValue.prefix ?? ''}</tspan>
|
||||
<tspan>{displayValue.text}</tspan>
|
||||
<tspan className={styles.text} fontSize={unitFontSize} dy={suffixShift}>
|
||||
{displayValue.suffix ?? ''}
|
||||
</tspan>
|
||||
<tspan fontSize={unitFontSize}>{displayValue.suffix ?? ''}</tspan>
|
||||
</text>
|
||||
)}
|
||||
{showName && (
|
||||
@@ -143,7 +136,7 @@ export const RadialText = memo(
|
||||
x={centerX}
|
||||
y={nameY}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
dominantBaseline="text-bottom"
|
||||
fill={nameColor}
|
||||
>
|
||||
{displayValue.title}
|
||||
@@ -155,9 +148,3 @@ export const RadialText = memo(
|
||||
);
|
||||
|
||||
RadialText.displayName = 'RadialText';
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2) => ({
|
||||
text: css({
|
||||
verticalAlign: 'bottom',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 110 A 90 90 0 1 1 149.98429203681178 110.00000137077838 A 10 10 0 0 1 149.98778269529805 130.00000106616096 A 70 70 0 1 0 150 130 A 10 10 0 0 1 150 110 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 120 A 80 80 0 1 1 149.98603736605492 120.00000121846968"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 10 A 90 90 0 0 1 100 190 L 100 170 A 70 70 0 0 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 17.5 A 82.5 82.5 0 0 1 100 182.5 L 100 177.5 A 77.5 77.5 0 0 0 100 22.5 L 100 17.5 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 40 A 60 60 0 0 1 100 160 L 100 140 A 40 40 0 0 0 100 60 L 100 40 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 50 A 50 50 0 0 1 100 150"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 10 A 90 90 0 0 1 190 100 L 170 100 A 70 70 0 0 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 20 A 80 80 0 0 1 180 100"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 A 10 10 0 0 1 30 100.00000000000001 A 70 70 0 1 0 100 30 A 10 10 0 0 1 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 L 30 100.00000000000001 A 70 70 0 1 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 -5 A 105 105 0 0 1 100 205 L 100 155 A 55 55 0 0 0 100 45 L 100 -5 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaultsDeep } from 'lodash';
|
||||
|
||||
import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data';
|
||||
import { createTheme, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data';
|
||||
import { FieldColorModeId } from '@grafana/schema';
|
||||
|
||||
import {
|
||||
@@ -50,35 +50,9 @@ describe('RadialGauge color utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
it('should return the baseColor if gradient is false-y', () => {
|
||||
expect(
|
||||
buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000')
|
||||
).toEqual([
|
||||
{ color: '#FF0000', percent: 0 },
|
||||
{ color: '#FF0000', percent: 1 },
|
||||
]);
|
||||
|
||||
expect(
|
||||
buildGradientColors(undefined, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000')
|
||||
).toEqual([
|
||||
{ color: '#FF0000', percent: 0 },
|
||||
{ color: '#FF0000', percent: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the fallback color if no baseColor is set', () => {
|
||||
expect(buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)))).toEqual(
|
||||
[
|
||||
{ color: FALLBACK_COLOR, percent: 0 },
|
||||
{ color: FALLBACK_COLOR, percent: 1 },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme(),
|
||||
buildFieldDisplay(createField(FieldColorModeId.Thresholds), {
|
||||
view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) },
|
||||
@@ -89,14 +63,13 @@ describe('RadialGauge color utils', () => {
|
||||
|
||||
it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => {
|
||||
expect(
|
||||
buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000')
|
||||
buildGradientColors(createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return gradient colors for continuous color modes', () => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme(),
|
||||
buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)),
|
||||
'#00FF00'
|
||||
@@ -107,7 +80,6 @@ describe('RadialGauge color utils', () => {
|
||||
it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme({ colors: { mode } }),
|
||||
buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues))
|
||||
)
|
||||
@@ -117,7 +89,6 @@ describe('RadialGauge color utils', () => {
|
||||
it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme({ colors: { mode } }),
|
||||
buildFieldDisplay(createField(FieldColorModeId.Fixed)),
|
||||
'#442299'
|
||||
|
||||
@@ -7,18 +7,10 @@ import { GradientStop, RadialShape } from './types';
|
||||
import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils';
|
||||
|
||||
export function buildGradientColors(
|
||||
gradient = false,
|
||||
theme: GrafanaTheme2,
|
||||
fieldDisplay: FieldDisplay,
|
||||
baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR
|
||||
): GradientStop[] {
|
||||
if (!gradient) {
|
||||
return [
|
||||
{ color: baseColor, percent: 0 },
|
||||
{ color: baseColor, percent: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode);
|
||||
|
||||
// thresholds get special handling
|
||||
|
||||
@@ -2,14 +2,20 @@ import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { RadialGaugeDimensions } from './types';
|
||||
|
||||
// some utility transparent white colors for gradients
|
||||
const TRANSPARENT_WHITE = '#ffffff00';
|
||||
const MOSTLY_TRANSPARENT_WHITE = '#ffffff88';
|
||||
const MOSTLY_OPAQUE_WHITE = '#ffffffbb';
|
||||
const OPAQUE_WHITE = '#ffffff';
|
||||
|
||||
const MIN_GLOW_SIZE = 0.75;
|
||||
const GLOW_FACTOR = 0.08;
|
||||
|
||||
export interface GlowGradientProps {
|
||||
id: string;
|
||||
barWidth: number;
|
||||
}
|
||||
|
||||
const MIN_GLOW_SIZE = 0.75;
|
||||
const GLOW_FACTOR = 0.08;
|
||||
|
||||
export function GlowGradient({ id, barWidth }: GlowGradientProps) {
|
||||
// 0.75 is the minimum glow size, and it scales with bar width
|
||||
const glowSize = MIN_GLOW_SIZE + barWidth * GLOW_FACTOR;
|
||||
@@ -27,16 +33,6 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) {
|
||||
|
||||
const CENTER_GLOW_OPACITY = 0.25;
|
||||
|
||||
export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) {
|
||||
const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY);
|
||||
return (
|
||||
<radialGradient id={`circle-glow-${gaugeId}`} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor={'#ffffff00'} />
|
||||
</radialGradient>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CenterGlowProps {
|
||||
dimensions: RadialGaugeDimensions;
|
||||
gaugeId: string;
|
||||
@@ -52,7 +48,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps
|
||||
<defs>
|
||||
<radialGradient id={gradientId} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor="#ffffff00" />
|
||||
<stop offset="90%" stopColor={TRANSPARENT_WHITE} />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
@@ -62,19 +58,15 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps
|
||||
);
|
||||
}
|
||||
|
||||
export function SpotlightGradient({
|
||||
id,
|
||||
dimensions,
|
||||
roundedBars,
|
||||
angle,
|
||||
theme,
|
||||
}: {
|
||||
interface SpotlightGradientProps {
|
||||
id: string;
|
||||
dimensions: RadialGaugeDimensions;
|
||||
angle: number;
|
||||
roundedBars: boolean;
|
||||
theme: GrafanaTheme2;
|
||||
}) {
|
||||
}
|
||||
|
||||
export function SpotlightGradient({ id, dimensions, roundedBars, angle, theme }: SpotlightGradientProps) {
|
||||
if (theme.isLight) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,9 +80,9 @@ export function SpotlightGradient({
|
||||
|
||||
return (
|
||||
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor="#ffffff00" />
|
||||
<stop offset="95%" stopColor="#ffffff88" />
|
||||
{roundedBars && <stop offset="100%" stopColor={roundedBars ? '#ffffffbb' : 'white'} />}
|
||||
<stop offset="0%" stopColor={TRANSPARENT_WHITE} />
|
||||
<stop offset="95%" stopColor={MOSTLY_TRANSPARENT_WHITE} />
|
||||
{roundedBars && <stop offset="100%" stopColor={roundedBars ? MOSTLY_OPAQUE_WHITE : OPAQUE_WHITE} />}
|
||||
</linearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'non
|
||||
export type RadialShape = 'circle' | 'gauge';
|
||||
|
||||
export interface RadialGaugeDimensions {
|
||||
vizHeight: number;
|
||||
vizWidth: number;
|
||||
margin: number;
|
||||
radius: number;
|
||||
centerX: number;
|
||||
|
||||
@@ -283,7 +283,9 @@ describe('RadialGauge utils', () => {
|
||||
});
|
||||
|
||||
describe('drawRadialArcPath', () => {
|
||||
const defaultDims: RadialGaugeDimensions = Object.freeze({
|
||||
const defaultDims = Object.freeze({
|
||||
vizHeight: 220,
|
||||
vizWidth: 220,
|
||||
centerX: 100,
|
||||
centerY: 100,
|
||||
radius: 80,
|
||||
@@ -297,7 +299,7 @@ describe('RadialGauge utils', () => {
|
||||
scaleLabelsSpacing: 0,
|
||||
scaleLabelsRadius: 0,
|
||||
gaugeBottomY: 0,
|
||||
});
|
||||
}) satisfies RadialGaugeDimensions;
|
||||
|
||||
it.each([
|
||||
{ description: 'quarter arc', startAngle: 0, endAngle: 90 },
|
||||
@@ -324,11 +326,6 @@ describe('RadialGauge utils', () => {
|
||||
expect(drawRadialArcPath(0, 360, defaultDims)).toEqual(drawRadialArcPath(0, 359.99, defaultDims));
|
||||
expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims));
|
||||
});
|
||||
|
||||
it('should return empty string if inner radius collapses to zero or below', () => {
|
||||
const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 };
|
||||
expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -341,7 +338,9 @@ describe('RadialGauge utils', () => {
|
||||
|
||||
describe('getOptimalSegmentCount', () => {
|
||||
it('should adjust segment count based on dimensions and spacing', () => {
|
||||
const dimensions: RadialGaugeDimensions = {
|
||||
const dimensions = {
|
||||
vizHeight: 220,
|
||||
vizWidth: 220,
|
||||
centerX: 100,
|
||||
centerY: 100,
|
||||
radius: 80,
|
||||
@@ -355,7 +354,7 @@ describe('RadialGauge utils', () => {
|
||||
scaleLabelsSpacing: 0,
|
||||
scaleLabelsRadius: 0,
|
||||
gaugeBottomY: 0,
|
||||
};
|
||||
} satisfies RadialGaugeDimensions;
|
||||
|
||||
expect(getOptimalSegmentCount(dimensions, 2, 10, 360)).toBe(8);
|
||||
expect(getOptimalSegmentCount(dimensions, 1, 5, 360)).toBe(5);
|
||||
|
||||
@@ -155,6 +155,8 @@ export function calculateDimensions(
|
||||
}
|
||||
|
||||
return {
|
||||
vizWidth: width,
|
||||
vizHeight: height,
|
||||
margin,
|
||||
gaugeBottomY: centerY + belowCenterY,
|
||||
radius: innerRadius,
|
||||
@@ -185,7 +187,7 @@ export function drawRadialArcPath(
|
||||
dimensions: RadialGaugeDimensions,
|
||||
roundedBars?: boolean
|
||||
): string {
|
||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
||||
const { radius, centerX, centerY } = dimensions;
|
||||
|
||||
// For some reason a 100% full arc cannot be rendered
|
||||
if (endAngle >= 360) {
|
||||
@@ -197,66 +199,12 @@ export function drawRadialArcPath(
|
||||
|
||||
const largeArc = endAngle > 180 ? 1 : 0;
|
||||
|
||||
const outerR = radius + barWidth / 2;
|
||||
const innerR = Math.max(0, radius - barWidth / 2);
|
||||
if (innerR <= 0) {
|
||||
return ''; // cannot draw arc with 0 inner radius
|
||||
}
|
||||
let x1 = centerX + radius * Math.cos(startRadians);
|
||||
let y1 = centerY + radius * Math.sin(startRadians);
|
||||
let x2 = centerX + radius * Math.cos(endRadians);
|
||||
let y2 = centerY + radius * Math.sin(endRadians);
|
||||
|
||||
// get points for both an inner and outer arc. we draw
|
||||
// the arc entirely with a path's fill instead of using stroke
|
||||
// so that it can be used as a clip-path.
|
||||
const ox1 = centerX + outerR * Math.cos(startRadians);
|
||||
const oy1 = centerY + outerR * Math.sin(startRadians);
|
||||
const ox2 = centerX + outerR * Math.cos(endRadians);
|
||||
const oy2 = centerY + outerR * Math.sin(endRadians);
|
||||
|
||||
const ix1 = centerX + innerR * Math.cos(startRadians);
|
||||
const iy1 = centerY + innerR * Math.sin(startRadians);
|
||||
const ix2 = centerX + innerR * Math.cos(endRadians);
|
||||
const iy2 = centerY + innerR * Math.sin(endRadians);
|
||||
|
||||
// calculate the cap width in case we're drawing rounded bars
|
||||
const capR = barWidth / 2;
|
||||
|
||||
const pathParts = [
|
||||
// start at outer start
|
||||
'M',
|
||||
ox1,
|
||||
oy1,
|
||||
// outer arc from start to end (clockwise)
|
||||
'A',
|
||||
outerR,
|
||||
outerR,
|
||||
0,
|
||||
largeArc,
|
||||
1,
|
||||
ox2,
|
||||
oy2,
|
||||
];
|
||||
|
||||
if (roundedBars) {
|
||||
// rounded end cap: small arc connecting outer end to inner end
|
||||
pathParts.push('A', capR, capR, 0, 0, 1, ix2, iy2);
|
||||
} else {
|
||||
// straight line to inner end (square butt)
|
||||
pathParts.push('L', ix2, iy2);
|
||||
}
|
||||
|
||||
// inner arc from end back to start (counter-clockwise)
|
||||
pathParts.push('A', innerR, innerR, 0, largeArc, 0, ix1, iy1);
|
||||
|
||||
if (roundedBars) {
|
||||
// rounded start cap: small arc connecting inner start back to outer start
|
||||
pathParts.push('A', capR, capR, 0, 0, 1, ox1, oy1);
|
||||
} else {
|
||||
// straight line back to outer start (square butt)
|
||||
pathParts.push('L', ox1, oy1);
|
||||
}
|
||||
|
||||
pathParts.push('Z');
|
||||
|
||||
return pathParts.join(' ');
|
||||
return ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' ');
|
||||
}
|
||||
|
||||
export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) {
|
||||
|
||||
@@ -1108,12 +1108,18 @@ export function parseStyleJson(rawValue: unknown): CSSProperties | void {
|
||||
}
|
||||
}
|
||||
|
||||
// Safari 26 introduced rendering bugs which require us to disable several features of the table.
|
||||
// Safari 26.0 introduced rendering bugs which require us to disable several features of the table.
|
||||
// The bugs were later fixed in Safari 26.2.
|
||||
export const IS_SAFARI_26 = (() => {
|
||||
if (navigator == null) {
|
||||
return false;
|
||||
}
|
||||
const userAgent = navigator.userAgent;
|
||||
const safariVersionMatch = userAgent.match(/Version\/(\d+)\./);
|
||||
return safariVersionMatch && parseInt(safariVersionMatch[1], 10) === 26;
|
||||
const safariVersionMatch = userAgent.match(/Version\/(\d+)\.(\d+)/);
|
||||
if (!safariVersionMatch) {
|
||||
return false;
|
||||
}
|
||||
const majorVersion = +safariVersionMatch[1];
|
||||
const minorVersion = +safariVersionMatch[2];
|
||||
return majorVersion === 26 && minorVersion <= 1;
|
||||
})();
|
||||
|
||||
@@ -66,6 +66,6 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { UserView } from './types';
|
||||
|
||||
export interface UserIconProps {
|
||||
/** An object that contains the user's details and 'lastActiveAt' status */
|
||||
/** An object that contains the user's details and an optional 'lastActiveAt' status */
|
||||
userView: UserView;
|
||||
/** A boolean value that determines whether the tooltip should be shown or not */
|
||||
showTooltip?: boolean;
|
||||
@@ -64,7 +64,8 @@ export const UserIcon = ({
|
||||
showTooltip = true,
|
||||
}: PropsWithChildren<UserIconProps>) => {
|
||||
const { user, lastActiveAt } = userView;
|
||||
const isActive = dateTime(lastActiveAt).diff(dateTime(), 'minutes', true) >= -15;
|
||||
const hasActive = lastActiveAt !== undefined && lastActiveAt !== null;
|
||||
const isActive = hasActive && dateTime(lastActiveAt).diff(dateTime(), 'minutes', true) >= -15;
|
||||
const theme = useTheme2();
|
||||
const styles = useMemo(() => getStyles(theme, isActive), [theme, isActive]);
|
||||
const content = (
|
||||
@@ -88,18 +89,20 @@ export const UserIcon = ({
|
||||
const tooltip = (
|
||||
<div className={styles.tooltipContainer}>
|
||||
<div className={styles.tooltipName}>{user.name}</div>
|
||||
<div className={styles.tooltipDate}>
|
||||
{isActive ? (
|
||||
<div className={styles.dotContainer}>
|
||||
<span>
|
||||
<Trans i18nKey="grafana-ui.user-icon.active-text">Active last 15m</Trans>
|
||||
</span>
|
||||
<span className={styles.dot}></span>
|
||||
</div>
|
||||
) : (
|
||||
formatViewed(lastActiveAt)
|
||||
)}
|
||||
</div>
|
||||
{hasActive && (
|
||||
<div className={styles.tooltipDate}>
|
||||
{isActive ? (
|
||||
<div className={styles.dotContainer}>
|
||||
<span>
|
||||
<Trans i18nKey="grafana-ui.user-icon.active-text">Active last 15m</Trans>
|
||||
</span>
|
||||
<span className={styles.dot}></span>
|
||||
</div>
|
||||
) : (
|
||||
formatViewed(lastActiveAt)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -60,6 +60,6 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UserIcon } from './UserIcon';
|
||||
import { UserView } from './types';
|
||||
|
||||
export interface UsersIndicatorProps {
|
||||
/** An object that contains the user's details and 'lastActiveAt' status */
|
||||
/** An object that contains the user's details and an optional 'lastActiveAt' status */
|
||||
users: UserView[];
|
||||
/** A limit of how many user icons to show before collapsing them and showing a number of users instead */
|
||||
limit?: number;
|
||||
@@ -40,7 +40,7 @@ export const UsersIndicator = ({ users, onClick, limit = 4 }: UsersIndicatorProp
|
||||
aria-label={t('grafana-ui.users-indicator.container-label', 'Users indicator container')}
|
||||
>
|
||||
{limitReached && (
|
||||
<UserIcon onClick={onClick} userView={{ user: { name: 'Extra users' }, lastActiveAt: '' }} showTooltip={false}>
|
||||
<UserIcon onClick={onClick} userView={{ user: { name: 'Extra users' } }} showTooltip={false}>
|
||||
{tooManyUsers
|
||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'...'
|
||||
|
||||
@@ -8,5 +8,5 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
|
||||
@@ -226,8 +226,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
|
||||
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
|
||||
}
|
||||
|
||||
@@ -410,8 +410,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
|
||||
|
||||
if !hasEnabledProviders {
|
||||
|
||||
44
pkg/services/accesscontrol/acimpl/basic_role_db_seed.go
Normal file
44
pkg/services/accesscontrol/acimpl/basic_role_db_seed.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package acimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
ossBasicRoleSeedLockName = "oss-ac-basic-role-seeder"
|
||||
ossBasicRoleSeedTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
// refreshBasicRolePermissionsInDB ensures basic role permissions are fully derived from in-memory registrations
|
||||
func (s *Service) refreshBasicRolePermissionsInDB(ctx context.Context, rolesSnapshot map[string][]accesscontrol.Permission) error {
|
||||
if s.sql == nil || s.seeder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
run := func(ctx context.Context) error {
|
||||
desired := map[accesscontrol.SeedPermission]struct{}{}
|
||||
for role, permissions := range rolesSnapshot {
|
||||
for _, permission := range permissions {
|
||||
desired[accesscontrol.SeedPermission{BuiltInRole: role, Action: permission.Action, Scope: permission.Scope}] = struct{}{}
|
||||
}
|
||||
}
|
||||
s.seeder.SetDesiredPermissions(desired)
|
||||
return s.seeder.Seed(ctx)
|
||||
}
|
||||
|
||||
if s.serverLock == nil {
|
||||
return run(ctx)
|
||||
}
|
||||
|
||||
var err error
|
||||
errLock := s.serverLock.LockExecuteAndRelease(ctx, ossBasicRoleSeedLockName, ossBasicRoleSeedTimeout, func(ctx context.Context) {
|
||||
err = run(ctx)
|
||||
})
|
||||
if errLock != nil {
|
||||
return errLock
|
||||
}
|
||||
return err
|
||||
}
|
||||
128
pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go
Normal file
128
pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package acimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestIntegration_OSSBasicRolePermissions_PersistAndRefreshOnRegisterFixedRoles(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
ctx := context.Background()
|
||||
sql := db.InitTestDB(t)
|
||||
store := database.ProvideService(sql)
|
||||
|
||||
svc := ProvideOSSService(
|
||||
setting.NewCfg(),
|
||||
store,
|
||||
&resourcepermissions.FakeActionSetSvc{},
|
||||
localcache.ProvideService(),
|
||||
featuremgmt.WithFeatures(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
sql,
|
||||
permreg.ProvidePermissionRegistry(),
|
||||
nil,
|
||||
)
|
||||
|
||||
require.NoError(t, svc.DeclareFixedRoles(accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:role",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "test:read", Scope: ""},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleViewer)},
|
||||
}))
|
||||
|
||||
require.NoError(t, svc.RegisterFixedRoles(ctx))
|
||||
|
||||
// verify permission is persisted to DB for basic:viewer
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
var count int64
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
return nil
|
||||
}))
|
||||
|
||||
// ensure RegisterFixedRoles refreshes it back to defaults
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
ts := time.Now()
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID)
|
||||
require.NoError(t, err)
|
||||
p := accesscontrol.Permission{
|
||||
RoleID: role.ID,
|
||||
Action: "custom:keep",
|
||||
Scope: "",
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope)
|
||||
_, err = sess.Table("permission").Insert(&p)
|
||||
return err
|
||||
}))
|
||||
|
||||
svc2 := ProvideOSSService(
|
||||
setting.NewCfg(),
|
||||
store,
|
||||
&resourcepermissions.FakeActionSetSvc{},
|
||||
localcache.ProvideService(),
|
||||
featuremgmt.WithFeatures(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
sql,
|
||||
permreg.ProvidePermissionRegistry(),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, svc2.DeclareFixedRoles(accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:role",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "test:read", Scope: ""},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleViewer)},
|
||||
}))
|
||||
require.NoError(t, svc2.RegisterFixedRoles(ctx))
|
||||
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
var count int64
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ?", role.ID, "custom:keep").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count)
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/seeding"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -96,6 +97,12 @@ func ProvideOSSService(
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
store: store,
|
||||
permRegistry: permRegistry,
|
||||
sql: db,
|
||||
serverLock: lock,
|
||||
}
|
||||
|
||||
if backend, ok := store.(*database.AccessControlStore); ok {
|
||||
s.seeder = seeding.New(log.New("accesscontrol.seeder"), backend, backend)
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -112,8 +119,11 @@ type Service struct {
|
||||
rolesMu sync.RWMutex
|
||||
roles map[string]*accesscontrol.RoleDTO
|
||||
store accesscontrol.Store
|
||||
seeder *seeding.Seeder
|
||||
permRegistry permreg.PermissionRegistry
|
||||
isInitialized bool
|
||||
sql db.DB
|
||||
serverLock *serverlock.ServerLockService
|
||||
}
|
||||
|
||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||
@@ -431,17 +441,54 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error {
|
||||
defer span.End()
|
||||
|
||||
s.rolesMu.Lock()
|
||||
defer s.rolesMu.Unlock()
|
||||
|
||||
registrations := s.registrations.Slice()
|
||||
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
s.registerRolesLocked(registration)
|
||||
return true
|
||||
})
|
||||
|
||||
s.isInitialized = true
|
||||
|
||||
rolesSnapshot := s.getBasicRolePermissionsLocked()
|
||||
s.rolesMu.Unlock()
|
||||
|
||||
if s.seeder != nil {
|
||||
if err := s.seeder.SeedRoles(ctx, registrations); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.seeder.RemoveAbsentRoles(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBasicRolePermissionsSnapshotFromRegistrationsLocked computes the desired basic role permissions from the
|
||||
// current registration list, using the shared seeding registration logic.
|
||||
//
|
||||
// it has to be called while holding the roles lock
|
||||
func (s *Service) getBasicRolePermissionsLocked() map[string][]accesscontrol.Permission {
|
||||
desired := map[accesscontrol.SeedPermission]struct{}{}
|
||||
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
seeding.AppendDesiredPermissions(desired, s.log, ®istration.Role, registration.Grants, registration.Exclude)
|
||||
return true
|
||||
})
|
||||
|
||||
out := make(map[string][]accesscontrol.Permission)
|
||||
for sp := range desired {
|
||||
out[sp.BuiltInRole] = append(out[sp.BuiltInRole], accesscontrol.Permission{
|
||||
Action: sp.Action,
|
||||
Scope: sp.Scope,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// registerRolesLocked processes a single role registration and adds permissions to basic roles.
|
||||
// Must be called with s.rolesMu locked.
|
||||
func (s *Service) registerRolesLocked(registration accesscontrol.RoleRegistration) {
|
||||
@@ -474,6 +521,7 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
||||
defer span.End()
|
||||
|
||||
acRegs := pluginutils.ToRegistrations(ID, name, regs)
|
||||
updatedBasicRoles := false
|
||||
for _, r := range acRegs {
|
||||
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
|
||||
return err
|
||||
@@ -500,11 +548,23 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
||||
if initialized {
|
||||
s.rolesMu.Lock()
|
||||
s.registerRolesLocked(r)
|
||||
updatedBasicRoles = true
|
||||
s.rolesMu.Unlock()
|
||||
s.cache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
if updatedBasicRoles {
|
||||
s.rolesMu.RLock()
|
||||
rolesSnapshot := s.getBasicRolePermissionsLocked()
|
||||
s.rolesMu.RUnlock()
|
||||
|
||||
// plugin roles can be declared after startup - keep DB in sync
|
||||
if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
623
pkg/services/accesscontrol/database/seeder.go
Normal file
623
pkg/services/accesscontrol/database/seeder.go
Normal file
@@ -0,0 +1,623 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/seeding"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/util/xorm/core"
|
||||
)
|
||||
|
||||
const basicRolePermBatchSize = 500
|
||||
|
||||
// LoadRoles returns all fixed and plugin roles (global org) with permissions, indexed by role name.
|
||||
func (s *AccessControlStore) LoadRoles(ctx context.Context) (map[string]*accesscontrol.RoleDTO, error) {
|
||||
out := map[string]*accesscontrol.RoleDTO{}
|
||||
|
||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
type roleRow struct {
|
||||
ID int64 `xorm:"id"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Version int64 `xorm:"version"`
|
||||
UID string `xorm:"uid"`
|
||||
Name string `xorm:"name"`
|
||||
DisplayName string `xorm:"display_name"`
|
||||
Description string `xorm:"description"`
|
||||
Group string `xorm:"group_name"`
|
||||
Hidden bool `xorm:"hidden"`
|
||||
Updated time.Time `xorm:"updated"`
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
roles := []roleRow{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
Where("(name LIKE ? OR name LIKE ?)", accesscontrol.FixedRolePrefix+"%", accesscontrol.PluginRolePrefix+"%").
|
||||
Find(&roles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
roleIDs := make([]any, 0, len(roles))
|
||||
roleByID := make(map[int64]*accesscontrol.RoleDTO, len(roles))
|
||||
for _, r := range roles {
|
||||
dto := &accesscontrol.RoleDTO{
|
||||
ID: r.ID,
|
||||
OrgID: r.OrgID,
|
||||
Version: r.Version,
|
||||
UID: r.UID,
|
||||
Name: r.Name,
|
||||
DisplayName: r.DisplayName,
|
||||
Description: r.Description,
|
||||
Group: r.Group,
|
||||
Hidden: r.Hidden,
|
||||
Updated: r.Updated,
|
||||
Created: r.Created,
|
||||
}
|
||||
out[dto.Name] = dto
|
||||
roleByID[dto.ID] = dto
|
||||
roleIDs = append(roleIDs, dto.ID)
|
||||
}
|
||||
|
||||
type permRow struct {
|
||||
RoleID int64 `xorm:"role_id"`
|
||||
Action string `xorm:"action"`
|
||||
Scope string `xorm:"scope"`
|
||||
}
|
||||
perms := []permRow{}
|
||||
if err := sess.Table("permission").In("role_id", roleIDs...).Find(&perms); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range perms {
|
||||
dto := roleByID[p.RoleID]
|
||||
if dto == nil {
|
||||
continue
|
||||
}
|
||||
dto.Permissions = append(dto.Permissions, accesscontrol.Permission{
|
||||
RoleID: p.RoleID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) SetRole(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error {
|
||||
if existingRole == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
_, err := sess.Table("role").
|
||||
Where("id = ? AND org_id = ?", existingRole.ID, accesscontrol.GlobalOrgID).
|
||||
Update(map[string]any{
|
||||
"display_name": wantedRole.DisplayName,
|
||||
"description": wantedRole.Description,
|
||||
"group_name": wantedRole.Group,
|
||||
"hidden": wantedRole.Hidden,
|
||||
"updated": time.Now(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) SetPermissions(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error {
|
||||
if existingRole == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type key struct{ Action, Scope string }
|
||||
existing := map[key]struct{}{}
|
||||
for _, p := range existingRole.Permissions {
|
||||
existing[key{p.Action, p.Scope}] = struct{}{}
|
||||
}
|
||||
desired := map[key]struct{}{}
|
||||
for _, p := range wantedRole.Permissions {
|
||||
desired[key{p.Action, p.Scope}] = struct{}{}
|
||||
}
|
||||
|
||||
toAdd := make([]accesscontrol.Permission, 0)
|
||||
toRemove := make([]accesscontrol.SeedPermission, 0)
|
||||
|
||||
now := time.Now()
|
||||
for k := range desired {
|
||||
if _, ok := existing[k]; ok {
|
||||
continue
|
||||
}
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: existingRole.ID,
|
||||
Action: k.Action,
|
||||
Scope: k.Scope,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
toAdd = append(toAdd, perm)
|
||||
}
|
||||
|
||||
for k := range existing {
|
||||
if _, ok := desired[k]; ok {
|
||||
continue
|
||||
}
|
||||
toRemove = append(toRemove, accesscontrol.SeedPermission{Action: k.Action, Scope: k.Scope})
|
||||
}
|
||||
|
||||
if len(toAdd) == 0 && len(toRemove) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
if len(toRemove) > 0 {
|
||||
if err := DeleteRolePermissionTuples(sess, s.sql.GetDBType(), existingRole.ID, toRemove); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdd) > 0 {
|
||||
_, err := sess.InsertMulti(toAdd)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) CreateRole(ctx context.Context, role accesscontrol.RoleDTO) error {
|
||||
now := time.Now()
|
||||
uid := role.UID
|
||||
if uid == "" && (strings.HasPrefix(role.Name, accesscontrol.FixedRolePrefix) || strings.HasPrefix(role.Name, accesscontrol.PluginRolePrefix)) {
|
||||
uid = accesscontrol.PrefixedRoleUID(role.Name)
|
||||
}
|
||||
r := accesscontrol.Role{
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Version: role.Version,
|
||||
UID: uid,
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
Description: role.Description,
|
||||
Group: role.Group,
|
||||
Hidden: role.Hidden,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
if r.Version == 0 {
|
||||
r.Version = 1
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
if _, err := sess.Insert(&r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(role.Permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// De-duplicate permissions on (action, scope) to avoid unique constraint violations.
|
||||
// Some role definitions may accidentally include duplicates.
|
||||
type permKey struct{ Action, Scope string }
|
||||
seen := make(map[permKey]struct{}, len(role.Permissions))
|
||||
|
||||
perms := make([]accesscontrol.Permission, 0, len(role.Permissions))
|
||||
for _, p := range role.Permissions {
|
||||
k := permKey{Action: p.Action, Scope: p.Scope}
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: r.ID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
perms = append(perms, perm)
|
||||
}
|
||||
_, err := sess.InsertMulti(perms)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) DeleteRoles(ctx context.Context, roleUIDs []string) error {
|
||||
if len(roleUIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
uids := make([]any, 0, len(roleUIDs))
|
||||
for _, uid := range roleUIDs {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
type row struct {
|
||||
ID int64 `xorm:"id"`
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
rows := []row{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
In("uid", uids...).
|
||||
Find(&rows); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
roleIDs := make([]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
roleIDs = append(roleIDs, r.ID)
|
||||
}
|
||||
|
||||
// Remove permissions and assignments first to avoid FK issues (if enabled).
|
||||
{
|
||||
args := append([]any{"DELETE FROM permission WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM user_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM team_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM builtin_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := append([]any{"DELETE FROM role WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")", accesscontrol.GlobalOrgID}, uids...)
|
||||
_, err := sess.Exec(args...)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// OSS basic-role permission refresh uses seeding.Seeder.Seed() with a desired set computed in memory.
|
||||
// These methods implement the permission seeding part of seeding.SeedingBackend against the current permission table.
|
||||
func (s *AccessControlStore) LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
var out map[accesscontrol.SeedPermission]struct{}
|
||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
rows, err := LoadBasicRoleSeedPermissions(sess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out = make(map[accesscontrol.SeedPermission]struct{}, len(rows))
|
||||
for _, r := range rows {
|
||||
r.Origin = ""
|
||||
out[r] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) Apply(ctx context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error {
|
||||
rolesToUpgrade := seeding.RolesToUpgrade(added, removed)
|
||||
|
||||
// Run the same OSS apply logic as ossBasicRoleSeedBackend.Apply inside a single transaction.
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
defs := accesscontrol.BuildBasicRoleDefinitions()
|
||||
builtinToRoleID, err := EnsureBasicRolesExist(sess, defs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend := &ossBasicRoleSeedBackend{
|
||||
sess: sess,
|
||||
now: time.Now(),
|
||||
builtinToRoleID: builtinToRoleID,
|
||||
desired: nil,
|
||||
dbType: s.sql.GetDBType(),
|
||||
}
|
||||
if err := backend.Apply(ctx, added, removed, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return BumpBasicRoleVersions(sess, rolesToUpgrade)
|
||||
})
|
||||
}
|
||||
|
||||
// EnsureBasicRolesExist ensures the built-in basic roles exist in the role table and are bound in builtin_role.
|
||||
// It returns a mapping from builtin role name (for example "Admin") to role ID.
|
||||
func EnsureBasicRolesExist(sess *db.Session, defs map[string]*accesscontrol.RoleDTO) (map[string]int64, error) {
|
||||
uidToBuiltin := make(map[string]string, len(defs))
|
||||
uids := make([]any, 0, len(defs))
|
||||
for builtin, def := range defs {
|
||||
uidToBuiltin[def.UID] = builtin
|
||||
uids = append(uids, def.UID)
|
||||
}
|
||||
|
||||
type roleRow struct {
|
||||
ID int64 `xorm:"id"`
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
|
||||
rows := []roleRow{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
In("uid", uids...).
|
||||
Find(&rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
|
||||
builtinToRoleID := make(map[string]int64, len(defs))
|
||||
for _, r := range rows {
|
||||
br, ok := uidToBuiltin[r.UID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
builtinToRoleID[br] = r.ID
|
||||
}
|
||||
|
||||
for builtin, def := range defs {
|
||||
roleID, ok := builtinToRoleID[builtin]
|
||||
if !ok {
|
||||
role := accesscontrol.Role{
|
||||
OrgID: def.OrgID,
|
||||
Version: def.Version,
|
||||
UID: def.UID,
|
||||
Name: def.Name,
|
||||
DisplayName: def.DisplayName,
|
||||
Description: def.Description,
|
||||
Group: def.Group,
|
||||
Hidden: def.Hidden,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
if _, err := sess.Insert(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleID = role.ID
|
||||
builtinToRoleID[builtin] = roleID
|
||||
}
|
||||
|
||||
has, err := sess.Table("builtin_role").
|
||||
Where("role_id = ? AND role = ? AND org_id = ?", roleID, builtin, accesscontrol.GlobalOrgID).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
br := accesscontrol.BuiltinRole{
|
||||
RoleID: roleID,
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Role: builtin,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
if _, err := sess.Table("builtin_role").Insert(&br); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builtinToRoleID, nil
|
||||
}
|
||||
|
||||
// DeleteRolePermissionTuples deletes permissions for a single role by (action, scope) pairs.
|
||||
//
|
||||
// It uses a row-constructor IN clause where supported (MySQL, Postgres, SQLite) and falls back
|
||||
// to a WHERE ... OR ... form for MSSQL.
|
||||
func DeleteRolePermissionTuples(sess *db.Session, dbType core.DbType, roleID int64, perms []accesscontrol.SeedPermission) error {
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dbType == migrator.MSSQL {
|
||||
// MSSQL doesn't support (action, scope) IN ((?,?),(?,?)) row constructors.
|
||||
where := make([]string, 0, len(perms))
|
||||
args := make([]any, 0, 1+len(perms)*2)
|
||||
args = append(args, roleID)
|
||||
for _, p := range perms {
|
||||
where = append(where, "(action = ? AND scope = ?)")
|
||||
args = append(args, p.Action, p.Scope)
|
||||
}
|
||||
_, err := sess.Exec(
|
||||
append([]any{
|
||||
"DELETE FROM permission WHERE role_id = ? AND (" + strings.Join(where, " OR ") + ")",
|
||||
}, args...)...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
args := make([]any, 0, 1+len(perms)*2)
|
||||
args = append(args, roleID)
|
||||
for _, p := range perms {
|
||||
args = append(args, p.Action, p.Scope)
|
||||
}
|
||||
sql := "DELETE FROM permission WHERE role_id = ? AND (action, scope) IN (" +
|
||||
strings.Repeat("(?, ?),", len(perms)-1) + "(?, ?))"
|
||||
_, err := sess.Exec(append([]any{sql}, args...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
type ossBasicRoleSeedBackend struct {
|
||||
sess *db.Session
|
||||
now time.Time
|
||||
builtinToRoleID map[string]int64
|
||||
desired map[accesscontrol.SeedPermission]struct{}
|
||||
dbType core.DbType
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) LoadPrevious(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
rows, err := LoadBasicRoleSeedPermissions(b.sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[accesscontrol.SeedPermission]struct{}, len(rows))
|
||||
for _, r := range rows {
|
||||
// Ensure the key matches what OSS seeding uses (Origin is always empty for basic role refresh).
|
||||
r.Origin = ""
|
||||
out[r] = struct{}{}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) LoadDesired(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
return b.desired, nil
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) Apply(_ context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error {
|
||||
// Delete removed permissions (this includes user-defined permissions that aren't in desired).
|
||||
if len(removed) > 0 {
|
||||
permsByRoleID := map[int64][]accesscontrol.SeedPermission{}
|
||||
for _, p := range removed {
|
||||
roleID, ok := b.builtinToRoleID[p.BuiltInRole]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
permsByRoleID[roleID] = append(permsByRoleID[roleID], p)
|
||||
}
|
||||
|
||||
for roleID, perms := range permsByRoleID {
|
||||
// Chunk to keep statement sizes and parameter counts bounded.
|
||||
if err := batch(len(perms), basicRolePermBatchSize, func(start, end int) error {
|
||||
return DeleteRolePermissionTuples(b.sess, b.dbType, roleID, perms[start:end])
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert added permissions and updated-target permissions.
|
||||
toInsertSeed := make([]accesscontrol.SeedPermission, 0, len(added)+len(updated))
|
||||
toInsertSeed = append(toInsertSeed, added...)
|
||||
for _, v := range updated {
|
||||
toInsertSeed = append(toInsertSeed, v)
|
||||
}
|
||||
if len(toInsertSeed) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// De-duplicate on (role_id, action, scope). This avoids unique constraint violations when:
|
||||
// - the same permission appears in both added and updated
|
||||
// - multiple plugin origins grant the same permission (Origin is not persisted in permission table)
|
||||
type permKey struct {
|
||||
RoleID int64
|
||||
Action string
|
||||
Scope string
|
||||
}
|
||||
seen := make(map[permKey]struct{}, len(toInsertSeed))
|
||||
|
||||
toInsert := make([]accesscontrol.Permission, 0, len(toInsertSeed))
|
||||
for _, p := range toInsertSeed {
|
||||
roleID, ok := b.builtinToRoleID[p.BuiltInRole]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k := permKey{RoleID: roleID, Action: p.Action, Scope: p.Scope}
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: roleID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
Created: b.now,
|
||||
Updated: b.now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
toInsert = append(toInsert, perm)
|
||||
}
|
||||
|
||||
return batch(len(toInsert), basicRolePermBatchSize, func(start, end int) error {
|
||||
// MySQL: ignore conflicts to make seeding idempotent under retries/concurrency.
|
||||
// Conflicts can happen if the same permission already exists (unique on role_id, action, scope).
|
||||
if b.dbType == migrator.MySQL {
|
||||
args := make([]any, 0, (end-start)*8)
|
||||
for i := start; i < end; i++ {
|
||||
p := toInsert[i]
|
||||
args = append(args, p.RoleID, p.Action, p.Scope, p.Kind, p.Attribute, p.Identifier, p.Updated, p.Created)
|
||||
}
|
||||
sql := append([]any{`INSERT IGNORE INTO permission (role_id, action, scope, kind, attribute, identifier, updated, created) VALUES ` +
|
||||
strings.Repeat("(?, ?, ?, ?, ?, ?, ?, ?),", end-start-1) + "(?, ?, ?, ?, ?, ?, ?, ?)"}, args...)
|
||||
_, err := b.sess.Exec(sql...)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := b.sess.InsertMulti(toInsert[start:end])
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func batch(count, size int, eachFn func(start, end int) error) error {
|
||||
for i := 0; i < count; {
|
||||
end := i + size
|
||||
if end > count {
|
||||
end = count
|
||||
}
|
||||
if err := eachFn(i, end); err != nil {
|
||||
return err
|
||||
}
|
||||
i = end
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BumpBasicRoleVersions increments the role version for the given builtin basic roles (Viewer/Editor/Admin/Grafana Admin).
|
||||
// Unknown role names are ignored.
|
||||
func BumpBasicRoleVersions(sess *db.Session, basicRoles []string) error {
|
||||
if len(basicRoles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defs := accesscontrol.BuildBasicRoleDefinitions()
|
||||
uids := make([]any, 0, len(basicRoles))
|
||||
for _, br := range basicRoles {
|
||||
def, ok := defs[br]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
uids = append(uids, def.UID)
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sql := "UPDATE role SET version = version + 1 WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")"
|
||||
_, err := sess.Exec(append([]any{sql, accesscontrol.GlobalOrgID}, uids...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadBasicRoleSeedPermissions returns the current (builtin_role, action, scope) permissions granted to basic roles.
|
||||
// It sets Origin to empty.
|
||||
func LoadBasicRoleSeedPermissions(sess *db.Session) ([]accesscontrol.SeedPermission, error) {
|
||||
rows := []accesscontrol.SeedPermission{}
|
||||
err := sess.SQL(
|
||||
`SELECT role.display_name AS builtin_role, p.action, p.scope, '' AS origin
|
||||
FROM role INNER JOIN permission AS p ON p.role_id = role.id
|
||||
WHERE role.org_id = ? AND role.name LIKE 'basic:%'`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
).Find(&rows)
|
||||
return rows, err
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -130,6 +131,9 @@ func (r *ZanzanaReconciler) Run(ctx context.Context) error {
|
||||
// Reconcile schedules as job that will run and reconcile resources between
|
||||
// legacy access control and zanzana.
|
||||
func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
|
||||
// Ensure we don't reconcile an empty/partial RBAC state before OSS has seeded basic role permissions.
|
||||
// This matters most during startup where fixed-role loading + basic-role permission refresh runs as another background service.
|
||||
r.waitForBasicRolesSeeded(ctx)
|
||||
r.reconcile(ctx)
|
||||
|
||||
// FIXME:
|
||||
@@ -145,6 +149,57 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) hasBasicRolePermissions(ctx context.Context) bool {
|
||||
var count int64
|
||||
// Basic role permissions are stored on "basic:%" roles in the global org (0).
|
||||
// In a fresh DB, this will be empty until fixed roles are registered and the basic role permission refresh runs.
|
||||
type row struct {
|
||||
Count int64 `xorm:"count"`
|
||||
}
|
||||
_ = r.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var rr row
|
||||
_, err := sess.SQL(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM role INNER JOIN permission AS p ON p.role_id = role.id
|
||||
WHERE role.org_id = ? AND role.name LIKE ?`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
accesscontrol.BasicRolePrefix+"%",
|
||||
).Get(&rr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count = rr.Count
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) waitForBasicRolesSeeded(ctx context.Context) {
|
||||
// Best-effort: don't block forever. If we can't observe basic roles, proceed anyway.
|
||||
const (
|
||||
maxWait = 15 * time.Second
|
||||
interval = 1 * time.Second
|
||||
)
|
||||
|
||||
deadline := time.NewTimer(maxWait)
|
||||
defer deadline.Stop()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
if r.hasBasicRolePermissions(ctx) {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-deadline.C:
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
|
||||
run := func(ctx context.Context, namespace string) (ok bool) {
|
||||
now := time.Now()
|
||||
|
||||
67
pkg/services/accesscontrol/dualwrite/reconciler_test.go
Normal file
67
pkg/services/accesscontrol/dualwrite/reconciler_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func TestZanzanaReconciler_hasBasicRolePermissions(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
|
||||
r := &ZanzanaReconciler{
|
||||
store: env.db,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
require.False(t, r.hasBasicRolePermissions(ctx))
|
||||
|
||||
err := env.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := sess.Exec(
|
||||
`INSERT INTO role (org_id, uid, name, display_name, group_name, description, hidden, version, created, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
"basic_viewer_uid_test",
|
||||
accesscontrol.BasicRolePrefix+"viewer",
|
||||
"Viewer",
|
||||
"Basic",
|
||||
"Viewer role",
|
||||
false,
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var roleID int64
|
||||
if _, err := sess.SQL(`SELECT id FROM role WHERE org_id = ? AND uid = ?`, accesscontrol.GlobalOrgID, "basic_viewer_uid_test").Get(&roleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Exec(
|
||||
`INSERT INTO permission (role_id, action, scope, kind, attribute, identifier, created, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
roleID,
|
||||
"dashboards:read",
|
||||
"dashboards:*",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
now,
|
||||
now,
|
||||
)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, r.hasBasicRolePermissions(ctx))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -594,3 +595,18 @@ type QueryWithOrg struct {
|
||||
OrgId *int64 `json:"orgId"`
|
||||
Global bool `json:"global"`
|
||||
}
|
||||
|
||||
type SeedPermission struct {
|
||||
BuiltInRole string `xorm:"builtin_role"`
|
||||
Action string `xorm:"action"`
|
||||
Scope string `xorm:"scope"`
|
||||
Origin string `xorm:"origin"`
|
||||
}
|
||||
|
||||
type RoleStore interface {
|
||||
LoadRoles(ctx context.Context) (map[string]*RoleDTO, error)
|
||||
SetRole(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error
|
||||
SetPermissions(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error
|
||||
CreateRole(ctx context.Context, role RoleDTO) error
|
||||
DeleteRoles(ctx context.Context, roleUIDs []string) error
|
||||
}
|
||||
|
||||
452
pkg/services/accesscontrol/seeding/seeder.go
Normal file
452
pkg/services/accesscontrol/seeding/seeder.go
Normal file
@@ -0,0 +1,452 @@
|
||||
package seeding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
)
|
||||
|
||||
type Seeder struct {
|
||||
log log.Logger
|
||||
roleStore accesscontrol.RoleStore
|
||||
backend SeedingBackend
|
||||
builtinsPermissions map[accesscontrol.SeedPermission]struct{}
|
||||
seededFixedRoles map[string]bool
|
||||
seededPluginRoles map[string]bool
|
||||
seededPlugins map[string]bool
|
||||
hasSeededAlready bool
|
||||
}
|
||||
|
||||
// SeedingBackend provides the seed-set specific operations needed to seed.
|
||||
type SeedingBackend interface {
|
||||
// LoadPrevious returns the currently stored permissions for previously seeded roles.
|
||||
LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error)
|
||||
|
||||
// Apply updates the database to match the desired permissions.
|
||||
Apply(ctx context.Context,
|
||||
added, removed []accesscontrol.SeedPermission,
|
||||
updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission,
|
||||
) error
|
||||
}
|
||||
|
||||
func New(log log.Logger, roleStore accesscontrol.RoleStore, backend SeedingBackend) *Seeder {
|
||||
return &Seeder{
|
||||
log: log,
|
||||
roleStore: roleStore,
|
||||
backend: backend,
|
||||
builtinsPermissions: map[accesscontrol.SeedPermission]struct{}{},
|
||||
seededFixedRoles: map[string]bool{},
|
||||
seededPluginRoles: map[string]bool{},
|
||||
seededPlugins: map[string]bool{},
|
||||
hasSeededAlready: false,
|
||||
}
|
||||
}
|
||||
|
||||
// SetDesiredPermissions replaces the in-memory desired permission set used by Seed().
|
||||
func (s *Seeder) SetDesiredPermissions(desired map[accesscontrol.SeedPermission]struct{}) {
|
||||
if desired == nil {
|
||||
s.builtinsPermissions = map[accesscontrol.SeedPermission]struct{}{}
|
||||
return
|
||||
}
|
||||
s.builtinsPermissions = desired
|
||||
}
|
||||
|
||||
// Seed loads current and desired permissions, diffs them (including scope updates), applies changes, and bumps versions.
|
||||
func (s *Seeder) Seed(ctx context.Context) error {
|
||||
previous, err := s.backend.LoadPrevious(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// - Do not remove plugin permissions when the plugin didn't register this run (Origin set but not in seededPlugins).
|
||||
// - Preserve legacy plugin app access permissions in the persisted seed set (these are granted by default).
|
||||
if len(previous) > 0 {
|
||||
filtered := make(map[accesscontrol.SeedPermission]struct{}, len(previous))
|
||||
for p := range previous {
|
||||
// Legacy plugin app access permissions (Origin set) are granted by default and managed outside seeding.
|
||||
// Keep them out of the diff so seeding doesn't try to remove or "re-add" them on every run.
|
||||
if p.Action == pluginaccesscontrol.ActionAppAccess && p.Origin != "" {
|
||||
continue
|
||||
}
|
||||
if p.Origin != "" && !s.seededPlugins[p.Origin] {
|
||||
continue
|
||||
}
|
||||
filtered[p] = struct{}{}
|
||||
}
|
||||
previous = filtered
|
||||
}
|
||||
|
||||
added, removed, updated := s.permissionDiff(previous, s.builtinsPermissions)
|
||||
|
||||
if err := s.backend.Apply(ctx, added, removed, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedRoles populates the database with the roles and their assignments
|
||||
// It will create roles that do not exist and update roles that have changed
|
||||
// Do not use for provisioning. Validation is not enforced.
|
||||
func (s *Seeder) SeedRoles(ctx context.Context, registrationList []accesscontrol.RoleRegistration) error {
|
||||
roleMap, err := s.roleStore.LoadRoles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
missingRoles := make([]accesscontrol.RoleRegistration, 0, len(registrationList))
|
||||
|
||||
// Diff existing roles with the ones we want to seed.
|
||||
// If a role is missing, we add it to the missingRoles list
|
||||
for _, registration := range registrationList {
|
||||
registration := registration
|
||||
role, ok := roleMap[registration.Role.Name]
|
||||
switch {
|
||||
case registration.Role.IsFixed():
|
||||
s.seededFixedRoles[registration.Role.Name] = true
|
||||
case registration.Role.IsPlugin():
|
||||
s.seededPluginRoles[registration.Role.Name] = true
|
||||
// To be resilient to failed plugin loadings, we remember the plugins that have registered,
|
||||
// later we'll ignore permissions and roles of other plugins
|
||||
s.seededPlugins[pluginutils.PluginIDFromName(registration.Role.Name)] = true
|
||||
}
|
||||
|
||||
s.rememberPermissionAssignments(®istration.Role, registration.Grants, registration.Exclude)
|
||||
|
||||
if !ok {
|
||||
missingRoles = append(missingRoles, registration)
|
||||
continue
|
||||
}
|
||||
|
||||
if needsRoleUpdate(role, registration.Role) {
|
||||
if err := s.roleStore.SetRole(ctx, role, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if needsPermissionsUpdate(role, registration.Role) {
|
||||
if err := s.roleStore.SetPermissions(ctx, role, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, registration := range missingRoles {
|
||||
if err := s.roleStore.CreateRole(ctx, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func needsPermissionsUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool {
|
||||
if existingRole == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(existingRole.Permissions) != len(wantedRole.Permissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, p := range wantedRole.Permissions {
|
||||
found := false
|
||||
for _, ep := range existingRole.Permissions {
|
||||
if ep.Action == p.Action && ep.Scope == p.Scope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func needsRoleUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool {
|
||||
if existingRole == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Name != wantedRole.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if existingRole.DisplayName != wantedRole.DisplayName {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Description != wantedRole.Description {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Group != wantedRole.Group {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Hidden != wantedRole.Hidden {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Deprecated: SeedRole is deprecated and should not be used.
|
||||
// SeedRoles only does boot up seeding and should not be used for runtime seeding.
|
||||
func (s *Seeder) SeedRole(ctx context.Context, role accesscontrol.RoleDTO, builtInRoles []string) error {
|
||||
addedPermissions := make(map[string]struct{}, len(role.Permissions))
|
||||
permissions := make([]accesscontrol.Permission, 0, len(role.Permissions))
|
||||
for _, p := range role.Permissions {
|
||||
key := fmt.Sprintf("%s:%s", p.Action, p.Scope)
|
||||
if _, ok := addedPermissions[key]; !ok {
|
||||
addedPermissions[key] = struct{}{}
|
||||
permissions = append(permissions, accesscontrol.Permission{Action: p.Action, Scope: p.Scope})
|
||||
}
|
||||
}
|
||||
|
||||
wantedRole := accesscontrol.RoleDTO{
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Version: role.Version,
|
||||
UID: role.UID,
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
Description: role.Description,
|
||||
Group: role.Group,
|
||||
Permissions: permissions,
|
||||
Hidden: role.Hidden,
|
||||
}
|
||||
roleMap, err := s.roleStore.LoadRoles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingRole := roleMap[wantedRole.Name]
|
||||
if existingRole == nil {
|
||||
if err := s.roleStore.CreateRole(ctx, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if needsRoleUpdate(existingRole, wantedRole) {
|
||||
if err := s.roleStore.SetRole(ctx, existingRole, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if needsPermissionsUpdate(existingRole, wantedRole) {
|
||||
if err := s.roleStore.SetPermissions(ctx, existingRole, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remember seeded roles
|
||||
if wantedRole.IsFixed() {
|
||||
s.seededFixedRoles[wantedRole.Name] = true
|
||||
}
|
||||
isPluginRole := wantedRole.IsPlugin()
|
||||
if isPluginRole {
|
||||
s.seededPluginRoles[wantedRole.Name] = true
|
||||
|
||||
// To be resilient to failed plugin loadings, we remember the plugins that have registered,
|
||||
// later we'll ignore permissions and roles of other plugins
|
||||
s.seededPlugins[pluginutils.PluginIDFromName(role.Name)] = true
|
||||
}
|
||||
|
||||
s.rememberPermissionAssignments(&wantedRole, builtInRoles, []string{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Seeder) rememberPermissionAssignments(role *accesscontrol.RoleDTO, builtInRoles []string, excludedRoles []string) {
|
||||
AppendDesiredPermissions(s.builtinsPermissions, s.log, role, builtInRoles, excludedRoles)
|
||||
}
|
||||
|
||||
// AppendDesiredPermissions accumulates permissions from a role registration onto basic roles (Viewer/Editor/Admin/Grafana Admin).
|
||||
// - It expands parents via accesscontrol.BuiltInRolesWithParents.
|
||||
// - It can optionally ignore plugin app access permissions (which are granted by default).
|
||||
func AppendDesiredPermissions(
|
||||
out map[accesscontrol.SeedPermission]struct{},
|
||||
logger log.Logger,
|
||||
role *accesscontrol.RoleDTO,
|
||||
builtInRoles []string,
|
||||
excludedRoles []string,
|
||||
) {
|
||||
if out == nil || role == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for builtInRole := range accesscontrol.BuiltInRolesWithParents(builtInRoles) {
|
||||
// Skip excluded grants
|
||||
if slices.Contains(excludedRoles, builtInRole) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, perm := range role.Permissions {
|
||||
if role.IsPlugin() && perm.Action == pluginaccesscontrol.ActionAppAccess {
|
||||
logger.Debug("Role is attempting to grant access permission, but this permission is already granted by default and will be ignored",
|
||||
"role", role.Name, "permission", perm.Action, "scope", perm.Scope)
|
||||
continue
|
||||
}
|
||||
|
||||
sp := accesscontrol.SeedPermission{
|
||||
BuiltInRole: builtInRole,
|
||||
Action: perm.Action,
|
||||
Scope: perm.Scope,
|
||||
}
|
||||
|
||||
if role.IsPlugin() {
|
||||
sp.Origin = pluginutils.PluginIDFromName(role.Name)
|
||||
}
|
||||
|
||||
out[sp] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// permissionDiff returns:
|
||||
// - added: present in desired permissions, not in previous permissions
|
||||
// - removed: present in previous permissions, not in desired permissions
|
||||
// - updated: same role + action, but scope changed
|
||||
func (s *Seeder) permissionDiff(previous, desired map[accesscontrol.SeedPermission]struct{}) (added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) {
|
||||
addedSet := make(map[accesscontrol.SeedPermission]struct{}, 0)
|
||||
for n := range desired {
|
||||
if _, already := previous[n]; !already {
|
||||
addedSet[n] = struct{}{}
|
||||
} else {
|
||||
delete(previous, n)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of the new permissions is actually an old permission with an updated scope
|
||||
updated = make(map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, 0)
|
||||
for n := range addedSet {
|
||||
for p := range previous {
|
||||
if n.BuiltInRole == p.BuiltInRole && n.Action == p.Action {
|
||||
updated[p] = n
|
||||
delete(addedSet, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p := range addedSet {
|
||||
added = append(added, p)
|
||||
}
|
||||
|
||||
for p := range previous {
|
||||
if p.Action == pluginaccesscontrol.ActionAppAccess &&
|
||||
p.Scope != pluginaccesscontrol.ScopeProvider.GetResourceAllScope() {
|
||||
// Allows backward compatibility with plugins that have been seeded before the grant ignore rule was added
|
||||
s.log.Info("This permission already existed so it will not be removed",
|
||||
"role", p.BuiltInRole, "permission", p.Action, "scope", p.Scope)
|
||||
continue
|
||||
}
|
||||
|
||||
removed = append(removed, p)
|
||||
}
|
||||
|
||||
return added, removed, updated
|
||||
}
|
||||
|
||||
func (s *Seeder) ClearBasicRolesPluginPermissions(ID string) {
|
||||
removable := []accesscontrol.SeedPermission{}
|
||||
|
||||
for key := range s.builtinsPermissions {
|
||||
if matchPermissionByPluginID(key, ID) {
|
||||
removable = append(removable, key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, perm := range removable {
|
||||
delete(s.builtinsPermissions, perm)
|
||||
}
|
||||
}
|
||||
|
||||
func matchPermissionByPluginID(perm accesscontrol.SeedPermission, pluginID string) bool {
|
||||
if perm.Origin != pluginID {
|
||||
return false
|
||||
}
|
||||
actionTemplate := regexp.MustCompile(fmt.Sprintf("%s[.:]", pluginID))
|
||||
scopeTemplate := fmt.Sprintf(":%s", pluginID)
|
||||
return actionTemplate.MatchString(perm.Action) || strings.HasSuffix(perm.Scope, scopeTemplate)
|
||||
}
|
||||
|
||||
// RolesToUpgrade returns the unique basic roles that should have their version incremented.
|
||||
func RolesToUpgrade(added, removed []accesscontrol.SeedPermission) []string {
|
||||
set := map[string]struct{}{}
|
||||
for _, p := range added {
|
||||
set[p.BuiltInRole] = struct{}{}
|
||||
}
|
||||
for _, p := range removed {
|
||||
set[p.BuiltInRole] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(set))
|
||||
for r := range set {
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Seeder) ClearPluginRoles(ID string) {
|
||||
expectedPrefix := fmt.Sprintf("%s%s:", accesscontrol.PluginRolePrefix, ID)
|
||||
|
||||
for roleName := range s.seededPluginRoles {
|
||||
if strings.HasPrefix(roleName, expectedPrefix) {
|
||||
delete(s.seededPluginRoles, roleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Seeder) MarkSeededAlready() {
|
||||
s.hasSeededAlready = true
|
||||
}
|
||||
|
||||
func (s *Seeder) HasSeededAlready() bool {
|
||||
return s.hasSeededAlready
|
||||
}
|
||||
|
||||
func (s *Seeder) RemoveAbsentRoles(ctx context.Context) error {
|
||||
roleMap, errGet := s.roleStore.LoadRoles(ctx)
|
||||
if errGet != nil {
|
||||
s.log.Error("failed to get fixed roles from store", "err", errGet)
|
||||
return errGet
|
||||
}
|
||||
|
||||
toRemove := []string{}
|
||||
for _, r := range roleMap {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if r.IsFixed() {
|
||||
if !s.seededFixedRoles[r.Name] {
|
||||
s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name)
|
||||
toRemove = append(toRemove, r.UID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if r.IsPlugin() {
|
||||
if !s.seededPlugins[pluginutils.PluginIDFromName(r.Name)] {
|
||||
// To be resilient to failed plugin loadings
|
||||
// ignore stored roles related to plugins that have not registered this time
|
||||
s.log.Debug("plugin role has not been registered on this run skipping its removal", "role", r.Name)
|
||||
continue
|
||||
}
|
||||
if !s.seededPluginRoles[r.Name] {
|
||||
s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name)
|
||||
toRemove = append(toRemove, r.UID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errDelete := s.roleStore.DeleteRoles(ctx, toRemove); errDelete != nil {
|
||||
s.log.Error("failed to delete absent fixed and plugin roles", "err", errDelete)
|
||||
return errDelete
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package authnimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -83,8 +81,7 @@ func ProvideRegistration(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP)
|
||||
if !hasEnabledProviders {
|
||||
oauthInfos := socialService.GetOAuthInfoProviders()
|
||||
|
||||
@@ -1155,14 +1155,7 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "passwordlessMagicLinkAuthentication",
|
||||
Description: "Enable passwordless login via magic link authentication",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
{
|
||||
Name: "exploreMetricsRelatedLogs",
|
||||
Description: "Display Related Logs in Grafana Metrics Drilldown",
|
||||
Stage: FeatureStageExperimental,
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -160,7 +160,6 @@ timeRangePan,experimental,@grafana/dataviz-squad,false,false,true
|
||||
newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true
|
||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
|
||||
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
|
||||
prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true
|
||||
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -483,10 +483,6 @@ const (
|
||||
// Enables experimental reconciler for playlists
|
||||
FlagPlaylistsReconciler = "playlistsReconciler"
|
||||
|
||||
// FlagPasswordlessMagicLinkAuthentication
|
||||
// Enable passwordless login via magic link authentication
|
||||
FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication"
|
||||
|
||||
// FlagEnableExtensionsAdminPage
|
||||
// Enables the extension admin page regardless of development mode
|
||||
FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage"
|
||||
|
||||
3
pkg/services/featuremgmt/toggles_gen.json
generated
3
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -2661,7 +2661,8 @@
|
||||
"metadata": {
|
||||
"name": "passwordlessMagicLinkAuthentication",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z"
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z",
|
||||
"deletionTimestamp": "2026-01-08T15:33:29Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable passwordless login via magic link authentication",
|
||||
|
||||
@@ -33,8 +33,6 @@ import (
|
||||
)
|
||||
|
||||
func TestIntegrationFolderTreeZanzana(t *testing.T) {
|
||||
// TODO: Add back OSS seeding and enable this test
|
||||
t.Skip("Skipping folder tree test with Zanzana")
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
runIntegrationFolderTree(t, testinfra.GrafanaOpts{
|
||||
|
||||
@@ -70,8 +70,8 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
||||
// set initial values on activate
|
||||
this.setState({
|
||||
value: timeRange,
|
||||
from: timeRange.raw.from.toString(),
|
||||
to: timeRange.raw.to.toString(),
|
||||
from: typeof timeRange.raw.from === 'string' ? timeRange.raw.from : timeRange.raw.from.toISOString(),
|
||||
to: typeof timeRange.raw.to === 'string' ? timeRange.raw.to : timeRange.raw.to.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ export const usePluginConfig = (plugin?: CatalogPlugin) => {
|
||||
return loadPlugin(plugin.id);
|
||||
}
|
||||
return null;
|
||||
}, [plugin?.id, plugin?.isInstalled, plugin?.isDisabled]);
|
||||
}, [plugin?.id, plugin?.isInstalled, plugin?.isDisabled, plugin?.isFullyInstalled]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user