Merge remote-tracking branch 'origin/main' into jguer/update-delete-role-hook
This commit is contained in:
@@ -118,18 +118,18 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
|
||||
|
||||
+14
-14
@@ -314,15 +314,15 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -330,8 +330,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
@@ -355,23 +355,23 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -121,11 +121,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/admin/provisioning", reqOrgAdmin, hs.Index)
|
||||
r.Get("/admin/provisioning/*", reqOrgAdmin, hs.Index)
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
|
||||
r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index)
|
||||
}
|
||||
|
||||
// secrets management page
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) && hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatformUI) {
|
||||
r.Get("/admin/secrets", authorize(ac.EvalAny(
|
||||
ac.EvalPermission(secret.ActionSecretSecureValuesCreate),
|
||||
@@ -213,6 +215,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) {
|
||||
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))
|
||||
@@ -307,11 +310,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgRoute.Get("/quotas", authorize(ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas))
|
||||
})
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
|
||||
// Will eventually be replaced with the 'object' route
|
||||
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch) {
|
||||
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
|
||||
}
|
||||
|
||||
@@ -1262,7 +1262,7 @@ type GetHomeDashboardResponseBody struct {
|
||||
// swagger:response dashboardVersionsResponse
|
||||
type DashboardVersionsResponse struct {
|
||||
// in: body
|
||||
Body []dashver.DashboardVersionMeta `json:"body"`
|
||||
Body *dashver.DashboardVersionResponseMeta `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response dashboardVersionResponse
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
// r.Post("/api/snapshots/"
|
||||
func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesSnapshots) {
|
||||
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -34,6 +34,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
|
||||
|
||||
// metrics.go
|
||||
func (hs *HTTPServer) getDSQueryEndpoint() web.Handler {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagQueryServiceRewrite) {
|
||||
// rewrite requests from /ds/query to the new query service
|
||||
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
|
||||
|
||||
+1
-1
@@ -216,7 +216,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
hs.HooksService.RunIndexDataHooks(&data, c)
|
||||
|
||||
data.NavTree.ApplyCostManagementIA()
|
||||
data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string
|
||||
data.NavTree.Sort()
|
||||
|
||||
return &data, nil
|
||||
@@ -304,6 +303,7 @@ func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam str
|
||||
if pref.IsValidThemeID(themePrefId) {
|
||||
theme := pref.GetThemeByID(themePrefId)
|
||||
// TODO refactor
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !theme.IsExtra || hs.Features.IsEnabledGlobally(featuremgmt.FlagGrafanaconThemes) {
|
||||
return theme
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
|
||||
for providerName, provider := range oauthInfos {
|
||||
if provider.AutoLogin || hs.Cfg.OAuthAutoLogin {
|
||||
redirectUrl := hs.Cfg.AppSubURL + "/login/" + providerName
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
|
||||
redirectUrl += hs.getRedirectToForAutoLogin(c)
|
||||
}
|
||||
@@ -213,6 +214,7 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
|
||||
|
||||
if samlAutoLogin {
|
||||
redirectUrl := hs.Cfg.AppSubURL + "/login/saml"
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
|
||||
redirectUrl += hs.getRedirectToForAutoLogin(c)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) {
|
||||
|
||||
cookies.WriteCookie(reqCtx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
|
||||
cookies.WriteCookie(reqCtx.Resp, "redirectTo", redirectTo, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||
@@ -527,7 +528,7 @@ func callGetPluginAsset(sc *scenarioContext) {
|
||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string,
|
||||
cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||
require.NoError(t, err)
|
||||
|
||||
hs := HTTPServer{
|
||||
@@ -642,7 +643,7 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||
require.NoError(t, err)
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
@@ -832,7 +833,7 @@ func Test_PluginsSettings(t *testing.T) {
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||
require.NoError(t, err)
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
@@ -902,7 +903,7 @@ func Test_UpdatePluginSetting(t *testing.T) {
|
||||
|
||||
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||
require.NoError(t, err)
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
|
||||
func (hs *HTTPServer) registerShortURLAPI(apiRoute routing.RouteRegister) {
|
||||
reqSignedIn := middleware.ReqSignedIn
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
|
||||
handler := newShortURLK8sHandler(hs)
|
||||
apiRoute.Post("/api/short-urls", reqSignedIn, handler.createKubernetesShortURLsHandler)
|
||||
|
||||
@@ -45,8 +45,8 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
|
||||
@@ -96,16 +96,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.25.3
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37
|
||||
github.com/grafana/grafana-app-sdk/logging v0.46.0
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -84,14 +84,15 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
|
||||
+12
-12
@@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
@@ -207,8 +207,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -219,8 +219,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
@@ -237,21 +237,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -132,6 +132,7 @@ type NewBackendOpts struct {
|
||||
Tags []string
|
||||
Static bool
|
||||
WireTag string
|
||||
CGOEnabled bool
|
||||
GoBuildCache *dagger.CacheVolume
|
||||
GoModCache *dagger.CacheVolume
|
||||
}
|
||||
@@ -198,6 +199,12 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
goCacheProg = val
|
||||
}
|
||||
|
||||
cgoDisabled, err := options.Bool(flags.CGODisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cgoEnabled := !cgoDisabled
|
||||
|
||||
bopts := &backend.BuildOpts{
|
||||
Version: p.Version,
|
||||
Enterprise: p.Enterprise,
|
||||
@@ -206,6 +213,7 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
Static: static,
|
||||
WireTag: wireTag,
|
||||
Tags: tags,
|
||||
CGOEnabled: cgoEnabled,
|
||||
}
|
||||
|
||||
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
|
||||
@@ -233,9 +241,10 @@ func NewBackend(ctx context.Context, log *slog.Logger, artifact string, opts *Ne
|
||||
Tags: opts.Tags,
|
||||
Static: opts.Static,
|
||||
WireTag: opts.WireTag,
|
||||
CGOEnabled: opts.CGOEnabled,
|
||||
}
|
||||
|
||||
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution)
|
||||
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution, "cgo enabled", opts.CGOEnabled)
|
||||
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
|
||||
ArtifactString: artifact,
|
||||
Type: pipeline.ArtifactTypeDirectory,
|
||||
|
||||
@@ -119,7 +119,12 @@ func NewTarballFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments)
|
||||
cgoDisabled, err := options.Bool(flags.CGODisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cgoEnabled := !cgoDisabled
|
||||
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments, cgoEnabled)
|
||||
}
|
||||
|
||||
// NewTarball returns a properly initialized Tarball artifact.
|
||||
@@ -143,6 +148,7 @@ func NewTarball(
|
||||
goVersion string,
|
||||
viceroyVersion string,
|
||||
experiments []string,
|
||||
cgoEnabled bool,
|
||||
) (*pipeline.Artifact, error) {
|
||||
backendArtifact, err := NewBackend(ctx, log, artifact, &NewBackendOpts{
|
||||
Name: name,
|
||||
@@ -158,6 +164,7 @@ func NewTarball(
|
||||
Enterprise: enterprise,
|
||||
GoBuildCache: goBuildCache,
|
||||
GoModCache: goModCache,
|
||||
CGOEnabled: cgoEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -62,7 +62,7 @@ func Build(
|
||||
|
||||
ldflags := LDFlagsDynamic(vcsinfo)
|
||||
|
||||
if opts.Static {
|
||||
if opts.Static && opts.CGOEnabled {
|
||||
ldflags = LDFlagsStatic(vcsinfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,23 @@ type BuildOpts struct {
|
||||
GoCacheProg string
|
||||
Static bool
|
||||
Enterprise bool
|
||||
CGOEnabled bool
|
||||
}
|
||||
|
||||
func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc, error) {
|
||||
func distroOptsFunc(log *slog.Logger, distro Distribution, opts *BuildOpts) (DistroBuildOptsFunc, error) {
|
||||
if !opts.CGOEnabled {
|
||||
return func(distro Distribution, experiments, tags []string) *GoBuildOpts {
|
||||
os, arch := OSAndArch(distro)
|
||||
archv := ArchVersion(distro)
|
||||
return &GoBuildOpts{
|
||||
OS: os,
|
||||
Arch: arch,
|
||||
GoARM: GoARM(archv),
|
||||
CGOEnabled: false,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
if val, ok := DistributionGoOpts[distro]; ok {
|
||||
return DistroOptsLogger(log, val), nil
|
||||
}
|
||||
@@ -29,7 +43,7 @@ func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc,
|
||||
}
|
||||
|
||||
func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
|
||||
fn, err := distroOptsFunc(log, distro)
|
||||
fn, err := distroOptsFunc(log, distro, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,7 +53,7 @@ func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distributio
|
||||
}
|
||||
|
||||
func WithViceroyEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
|
||||
fn, err := distroOptsFunc(log, distro)
|
||||
fn, err := distroOptsFunc(log, distro, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,25 +105,30 @@ func GolangContainer(
|
||||
opts *BuildOpts,
|
||||
) (*dagger.Container, error) {
|
||||
os, _ := OSAndArch(distro)
|
||||
// Only use viceroy for all darwin and only windows/amd64
|
||||
if os == "darwin" || distro == DistWindowsAMD64 {
|
||||
// Only use viceroy for all darwin builds
|
||||
if opts.CGOEnabled && os == "darwin" {
|
||||
return ViceroyContainer(d, log, distro, goVersion, viceroyVersion, opts)
|
||||
}
|
||||
|
||||
container := golang.Container(d, platform, goVersion).
|
||||
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"mv", "/zig", "/bin/zig"}).
|
||||
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
|
||||
WithExec([]string{"mkdir", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
|
||||
container := golang.Container(d, platform, goVersion)
|
||||
|
||||
if opts.CGOEnabled {
|
||||
container = container.
|
||||
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"mv", "/zig", "/bin/zig"}).
|
||||
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
|
||||
WithExec([]string{"mkdir", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/x86_64-w64-mingw32-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/x86_64-w64-mingw32-cross.tgz", "-C", "/toolchain"})
|
||||
}
|
||||
return WithGoEnv(log, container, distro, opts)
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func BuildOptsStaticS390X(distro Distribution, experiments []string, tags []stri
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOptsStaticS390X builds Grafana statically for the s390x arch
|
||||
// BuildOptsStaticRiscv64 builds Grafana statically for the riscv64 arch
|
||||
func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, _ = OSAndArch(distro)
|
||||
@@ -280,6 +280,22 @@ func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []st
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOptsStaticWindows builds Grafana statically for Windows on amd64
|
||||
func BuildOptsStaticWindows(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, _ = OSAndArch(distro)
|
||||
)
|
||||
|
||||
return &GoBuildOpts{
|
||||
CC: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc",
|
||||
CXX: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-cpp",
|
||||
ExperimentalFlags: experiments,
|
||||
OS: os,
|
||||
Arch: "amd64",
|
||||
CGOEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func StdZigBuildOpts(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, arch = OSAndArch(distro)
|
||||
@@ -322,6 +338,19 @@ func ViceroyBuildOpts(distro Distribution, experiments []string, tags []string)
|
||||
}
|
||||
}
|
||||
|
||||
func BuildOptsNoCGO(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, arch = OSAndArch(distro)
|
||||
)
|
||||
|
||||
return &GoBuildOpts{
|
||||
ExperimentalFlags: experiments,
|
||||
OS: os,
|
||||
Arch: arch,
|
||||
CGOEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
var ZigTargets = map[Distribution]string{
|
||||
DistLinuxAMD64: "x86_64-linux-musl",
|
||||
DistLinuxAMD64Dynamic: "x86_64-linux-gnu",
|
||||
@@ -351,7 +380,7 @@ var DistributionGoOpts = map[Distribution]DistroBuildOptsFunc{
|
||||
|
||||
// Non-Linux distros can have whatever they want in CC and CXX; it'll get overridden
|
||||
// but it's probably not best to rely on that.
|
||||
DistWindowsAMD64: ViceroyBuildOpts,
|
||||
DistWindowsAMD64: BuildOptsStaticWindows,
|
||||
DistWindowsARM64: StdZigBuildOpts,
|
||||
DistDarwinAMD64: ViceroyBuildOpts,
|
||||
DistDarwinARM64: ViceroyBuildOpts,
|
||||
|
||||
@@ -82,6 +82,13 @@ func GoBuildEnv(opts *GoBuildOpts) []containers.Env {
|
||||
|
||||
// https://github.com/mattn/go-sqlite3/issues/1164#issuecomment-1635253695
|
||||
env = append(env, containers.EnvVar("CGO_CFLAGS", "-D_LARGEFILE64_SOURCE"))
|
||||
if opts.CC != "" {
|
||||
env = append(env, containers.EnvVar("CC", opts.CC))
|
||||
}
|
||||
|
||||
if opts.CXX != "" {
|
||||
env = append(env, containers.EnvVar("CXX", opts.CXX))
|
||||
}
|
||||
} else {
|
||||
env = append(env, containers.EnvVar("CGO_ENABLED", "0"))
|
||||
}
|
||||
@@ -90,14 +97,6 @@ func GoBuildEnv(opts *GoBuildOpts) []containers.Env {
|
||||
env = append(env, containers.EnvVar("GOEXPERIMENT", strings.Join(opts.ExperimentalFlags, ",")))
|
||||
}
|
||||
|
||||
if opts.CC != "" {
|
||||
env = append(env, containers.EnvVar("CC", opts.CC))
|
||||
}
|
||||
|
||||
if opts.CXX != "" {
|
||||
env = append(env, containers.EnvVar("CXX", opts.CXX))
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@ func ValidatePackage(ctx context.Context, d *dagger.Client, service *dagger.Serv
|
||||
|
||||
return c.WithServiceBinding("grafana", service).
|
||||
WithEnvVariable("GRAFANA_URL", "http://grafana:3000").
|
||||
WithEnvVariable("PW_TEST_HTML_REPORT_OPEN", "never").
|
||||
WithExec([]string{"yarn", "e2e:acceptance"}), nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
GoTags pipeline.FlagOption = "go-tag"
|
||||
GoExperiments pipeline.FlagOption = "go-experiments"
|
||||
Sign pipeline.FlagOption = "sign"
|
||||
CGODisabled pipeline.FlagOption = "nocgo"
|
||||
|
||||
// Pretty much only used to set the deb or RPM internal package name (and file name) to `{}-nightly` and/or `{}-rpi`
|
||||
Nightly pipeline.FlagOption = "nightly"
|
||||
@@ -81,6 +82,13 @@ var SignFlag = pipeline.Flag{
|
||||
},
|
||||
}
|
||||
|
||||
var CGODisabledFlag = pipeline.Flag{
|
||||
Name: "nocgo",
|
||||
Options: map[pipeline.FlagOption]any{
|
||||
CGODisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
var NightlyFlag = pipeline.Flag{
|
||||
Name: "nightly",
|
||||
Options: map[pipeline.FlagOption]any{
|
||||
@@ -95,5 +103,8 @@ func StdPackageFlags() []pipeline.Flag {
|
||||
return JoinFlags(
|
||||
distros,
|
||||
names,
|
||||
[]pipeline.Flag{
|
||||
CGODisabledFlag,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ require (
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.45.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/net v0.46.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/sync v0.17.0 // @grafana/alerting-backend
|
||||
golang.org/x/text v0.30.0 // indirect; @grafana/grafana-backend-group
|
||||
google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend
|
||||
|
||||
+2
-2
@@ -95,8 +95,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
|
||||
@@ -6,10 +6,10 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
golang.org/x/tools v0.37.0
|
||||
golang.org/x/tools v0.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
|
||||
+4
-4
@@ -6,10 +6,10 @@ require (
|
||||
cuelang.org/go v0.11.1
|
||||
github.com/dave/dst v0.27.3
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
|
||||
github.com/grafana/cog v0.0.43
|
||||
github.com/grafana/cog v0.0.44
|
||||
github.com/grafana/cuetsy v0.1.11
|
||||
github.com/matryer/is v1.4.1
|
||||
golang.org/x/tools v0.37.0
|
||||
golang.org/x/tools v0.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -47,8 +47,8 @@ require (
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yalue/merged_fs v1.3.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
+8
-8
@@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk=
|
||||
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ=
|
||||
github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
|
||||
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
|
||||
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
|
||||
@@ -100,16 +100,16 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
|
||||
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -333,6 +333,7 @@ func (s *Service) buildGraph(ctx context.Context, req *Request) (*simple.Directe
|
||||
case TypeCMDNode:
|
||||
node, err = buildCMDNode(ctx, rn, s.features, s.cfg)
|
||||
case TypeMLNode:
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagMlExpressions) {
|
||||
node, err = s.buildMLNode(dp, rn, req)
|
||||
if err != nil {
|
||||
|
||||
@@ -124,6 +124,7 @@ func buildCMDNode(ctx context.Context, rn *rawNode, toggles featuremgmt.FeatureT
|
||||
}
|
||||
|
||||
if commandType == TypeSQL {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !toggles.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) {
|
||||
return nil, fmt.Errorf("sql expressions are disabled")
|
||||
}
|
||||
|
||||
@@ -92,6 +92,12 @@ func ParseTracingConfig(cfg *setting.Cfg) (*TracingConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Allow overriding service name via configuration
|
||||
serviceName := section.Key("service_name").MustString("")
|
||||
if serviceName != "" {
|
||||
tc.ServiceName = serviceName
|
||||
}
|
||||
|
||||
// if sampler_type is set in tracing.opentelemetry, we ignore the config in tracing.jaeger
|
||||
sampler := section.Key("sampler_type").MustString("")
|
||||
if sampler != "" {
|
||||
|
||||
@@ -206,7 +206,8 @@ type Panel struct {
|
||||
// NewPanel creates a new Panel object.
|
||||
func NewPanel() *Panel {
|
||||
return &Panel{
|
||||
Transparent: (func(input bool) *bool { return &input })(false),
|
||||
Transparent: (func(input bool) *bool { return &input })(false),
|
||||
RepeatDirection: (func(input PanelRepeatDirection) *PanelRepeatDirection { return &input })(PanelRepeatDirectionH),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
|
||||
Buckets: sizeDefBuckets, // 100B ... ~1MB
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) {
|
||||
// the recommended default value from the prom_client
|
||||
// https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411
|
||||
@@ -70,6 +71,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
|
||||
reqDurationOptions.NativeHistogramMinResetDuration = time.Hour
|
||||
reqSizeOptions.NativeHistogramMinResetDuration = time.Hour
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagDisableClassicHTTPHistogram) {
|
||||
// setting Buckets to nil with native options set means the classic
|
||||
// histogram will no longer be exposed - this can be a good way to
|
||||
|
||||
@@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen
|
||||
require (
|
||||
cuelang.org/go v0.11.1
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
|
||||
github.com/grafana/cog v0.0.43
|
||||
github.com/grafana/cog v0.0.44
|
||||
github.com/grafana/cuetsy v0.1.11
|
||||
github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e
|
||||
)
|
||||
@@ -43,11 +43,11 @@ require (
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yalue/merged_fs v1.3.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+10
-10
@@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk=
|
||||
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ=
|
||||
github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
|
||||
github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -94,20 +94,20 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
|
||||
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+5
-5
@@ -97,14 +97,14 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.235.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||
|
||||
+12
-12
@@ -319,20 +319,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -352,20 +352,20 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -503,6 +503,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
|
||||
// Split dashboards when they are large
|
||||
var largeObjects apistore.LargeObjectSupport
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
|
||||
largeObjects = NewDashboardLargeObjectSupport(opts.Scheme, opts.StorageOpts.BlobThresholdBytes)
|
||||
storageOpts.LargeObjectSupport = largeObjects
|
||||
|
||||
@@ -147,7 +147,7 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
access.CanStar = user.IsIdentityType(authlib.TypeUser)
|
||||
|
||||
access.AnnotationsPermissions = &dashboard.AnnotationPermission{}
|
||||
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
|
||||
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, dashScope)
|
||||
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||
|
||||
title := obj.FindTitle("")
|
||||
|
||||
@@ -73,6 +73,7 @@ func RegisterAPIService(
|
||||
sql db.DB,
|
||||
reg prometheus.Registerer,
|
||||
) *SnapshotsAPIBuilder {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
|
||||
@@ -63,9 +63,11 @@ func RegisterAPIService(
|
||||
reg prometheus.Registerer,
|
||||
) (*DataSourceAPIBuilder, error) {
|
||||
// We want to expose just a limited set of plugins
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
explicitPluginList := features.IsEnabledGlobally(featuremgmt.FlagDatasourceAPIServers)
|
||||
|
||||
// This requires devmode!
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !explicitPluginList && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil, nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
@@ -111,6 +113,7 @@ func RegisterAPIService(
|
||||
datasources.GetDatasourceProvider(pluginJSON),
|
||||
contextProvider,
|
||||
accessControl,
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -155,6 +155,7 @@ func (s *folderStorage) setDefaultFolderPermissions(ctx context.Context, orgID i
|
||||
var permissions []accesscontrol.SetResourcePermissionCommand
|
||||
|
||||
isNested := parentUID != ""
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) && isNested {
|
||||
// No permissions on nested folders when kubernetesDashboards is enabled
|
||||
return nil
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *FolderAPIBuilder) afterDelete(obj runtime.Object, _ *metav1.DeleteOptio
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
||||
log.Info("Propagating deleted folder to Zanzana", "folder", meta.GetName(), "parent", meta.GetFolder())
|
||||
err = b.permissionStore.DeleteFolderParents(ctx, meta.GetNamespace(), meta.GetName())
|
||||
|
||||
@@ -288,6 +288,7 @@ func (b *FolderAPIBuilder) setDefaultFolderPermissions(ctx context.Context, key
|
||||
|
||||
func (b *FolderAPIBuilder) registerPermissionHooks(store *genericregistry.Store) {
|
||||
log := logging.FromContext(context.Background())
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
||||
log.Info("Enabling Zanzana folder propagation hooks")
|
||||
store.BeginCreate = b.beginCreate
|
||||
|
||||
+267
-15
@@ -97,16 +97,22 @@ func NewResourceTuple(object string, resource iamv0.ResourcePermissionspecResour
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// tupleToTupleKeyWithoutCondition converts a TupleKey to TupleKeyWithoutCondition
|
||||
// This is needed for delete operations which don't support conditions
|
||||
func tupleToTupleKeyWithoutCondition(tuple *v1.TupleKey) *v1.TupleKeyWithoutCondition {
|
||||
return &v1.TupleKeyWithoutCondition{
|
||||
User: tuple.User,
|
||||
Relation: tuple.Relation,
|
||||
Object: tuple.Object,
|
||||
}
|
||||
}
|
||||
|
||||
// toTupleKeysWithoutCondition converts v1.TupleKey to v1.TupleKeyWithoutCondition
|
||||
// by stripping the condition field, which is required for delete operations
|
||||
func toTupleKeysWithoutCondition(tuples []*v1.TupleKey) []*v1.TupleKeyWithoutCondition {
|
||||
result := make([]*v1.TupleKeyWithoutCondition, len(tuples))
|
||||
for i, t := range tuples {
|
||||
result[i] = &v1.TupleKeyWithoutCondition{
|
||||
User: t.User,
|
||||
Relation: t.Relation,
|
||||
Object: t.Object,
|
||||
}
|
||||
result[i] = tupleToTupleKeyWithoutCondition(t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -119,19 +125,29 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
|
||||
|
||||
rp, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
b.logger.Error("failed to convert object to resourcePermission type", "object", obj)
|
||||
return
|
||||
}
|
||||
|
||||
resourceType := "resourcepermission"
|
||||
operation := "create"
|
||||
|
||||
// Grab a ticket to write to Zanzana
|
||||
// This limits the amount of concurrent writes to Zanzana
|
||||
// This limits the amount of concurrent connections to Zanzana
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
|
||||
go func(rp *iamv0.ResourcePermission) {
|
||||
start := time.Now()
|
||||
status := "success"
|
||||
|
||||
defer func() {
|
||||
// Release the ticket after write is done
|
||||
<-b.zTickets
|
||||
// Record operation duration and count
|
||||
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
|
||||
hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
|
||||
}()
|
||||
|
||||
resource := rp.Spec.Resource
|
||||
@@ -157,6 +173,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
|
||||
// Avoid writing if there are no valid tuples
|
||||
if len(tuples) == 0 {
|
||||
b.logger.Warn("no valid tuples to write", "namespace", rp.Namespace, "resource", object)
|
||||
status = "failure"
|
||||
return
|
||||
}
|
||||
|
||||
@@ -176,12 +193,252 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
status = "failure"
|
||||
b.logger.Error("failed to write resource permission to zanzana",
|
||||
"err", err,
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"tuplesCnt", len(tuples),
|
||||
)
|
||||
} else {
|
||||
// Record successful tuple writes
|
||||
hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Add(float64(len(tuples)))
|
||||
}
|
||||
}(rp.DeepCopy()) // Pass a copy of the object
|
||||
}
|
||||
|
||||
// BeginResourcePermissionUpdate is a pre-update hook that prepares zanzana updates
|
||||
// It converts old and new permissions to tuples and performs the zanzana write after K8s update succeeds
|
||||
func (b *IdentityAccessManagementAPIBuilder) BeginResourcePermissionUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) {
|
||||
if b.zClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Extract permissions from both old and new objects
|
||||
oldRP, ok := oldObj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
newRP, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert old permissions to tuples for deletion
|
||||
var oldTuples []*v1.TupleKey
|
||||
if len(oldRP.Spec.Permissions) > 0 {
|
||||
oldResource := oldRP.Spec.Resource
|
||||
oldObject := zanzana.NewObjectEntry(toZanzanaType(oldResource.ApiGroup), oldResource.ApiGroup, oldResource.Resource, "", oldResource.Name)
|
||||
|
||||
oldTuples = make([]*v1.TupleKey, 0, len(oldRP.Spec.Permissions))
|
||||
for _, p := range oldRP.Spec.Permissions {
|
||||
tuple, err := NewResourceTuple(oldObject, oldResource, p)
|
||||
if err != nil {
|
||||
b.logger.Error("failed to create old resource permission tuple",
|
||||
"namespace", oldRP.Namespace,
|
||||
"object", oldObject,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
oldTuples = append(oldTuples, tuple)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert new permissions to tuples for writing
|
||||
var newTuples []*v1.TupleKey
|
||||
if len(newRP.Spec.Permissions) > 0 {
|
||||
newResource := newRP.Spec.Resource
|
||||
newObject := zanzana.NewObjectEntry(toZanzanaType(newResource.ApiGroup), newResource.ApiGroup, newResource.Resource, "", newResource.Name)
|
||||
|
||||
newTuples = make([]*v1.TupleKey, 0, len(newRP.Spec.Permissions))
|
||||
for _, p := range newRP.Spec.Permissions {
|
||||
tuple, err := NewResourceTuple(newObject, newResource, p)
|
||||
if err != nil {
|
||||
b.logger.Error("failed to create new resource permission tuple",
|
||||
"namespace", newRP.Namespace,
|
||||
"object", newObject,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
newTuples = append(newTuples, tuple)
|
||||
}
|
||||
}
|
||||
|
||||
// Return a finish function that performs the zanzana write only on success
|
||||
return func(ctx context.Context, success bool) {
|
||||
if !success {
|
||||
// Update failed, don't write to zanzana
|
||||
return
|
||||
}
|
||||
|
||||
// Grab a ticket to write to Zanzana
|
||||
// This limits the amount of concurrent connections to Zanzana
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.WithLabelValues("resourcepermission", "update").Observe(time.Since(wait).Seconds())
|
||||
|
||||
go func() {
|
||||
start := time.Now()
|
||||
status := "success"
|
||||
|
||||
defer func() {
|
||||
<-b.zTickets
|
||||
// Record operation duration and count
|
||||
hooksDurationHistogram.WithLabelValues("resourcepermission", "update", status).Observe(time.Since(start).Seconds())
|
||||
hooksOperationCounter.WithLabelValues("resourcepermission", "update", status).Inc()
|
||||
}()
|
||||
|
||||
b.logger.Debug("updating resource permission in zanzana",
|
||||
"namespace", newRP.Namespace,
|
||||
"oldPermissionsCnt", len(oldRP.Spec.Permissions),
|
||||
"newPermissionsCnt", len(newRP.Spec.Permissions),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Prepare write request
|
||||
req := &v1.WriteRequest{
|
||||
Namespace: newRP.Namespace,
|
||||
}
|
||||
|
||||
// Add deletes for old tuples
|
||||
if len(oldTuples) > 0 {
|
||||
deleteTuples := toTupleKeysWithoutCondition(oldTuples)
|
||||
req.Deletes = &v1.WriteRequestDeletes{
|
||||
TupleKeys: deleteTuples,
|
||||
}
|
||||
b.logger.Debug("deleting existing resource permissions from zanzana",
|
||||
"namespace", newRP.Namespace,
|
||||
"tuplesCnt", len(deleteTuples),
|
||||
)
|
||||
}
|
||||
|
||||
// Add writes for new tuples
|
||||
if len(newTuples) > 0 {
|
||||
req.Writes = &v1.WriteRequestWrites{
|
||||
TupleKeys: newTuples,
|
||||
}
|
||||
b.logger.Debug("writing new resource permissions to zanzana",
|
||||
"namespace", newRP.Namespace,
|
||||
"tuplesCnt", len(newTuples),
|
||||
)
|
||||
}
|
||||
|
||||
// Only make the request if there are deletes or writes
|
||||
if (req.Deletes != nil && len(req.Deletes.TupleKeys) > 0) || (req.Writes != nil && len(req.Writes.TupleKeys) > 0) {
|
||||
err := b.zClient.Write(ctx, req)
|
||||
if err != nil {
|
||||
status = "failure"
|
||||
b.logger.Error("failed to update resource permission in zanzana",
|
||||
"err", err,
|
||||
"namespace", newRP.Namespace,
|
||||
)
|
||||
} else {
|
||||
// Record successful tuple operations
|
||||
if len(oldTuples) > 0 {
|
||||
hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "delete").Add(float64(len(oldTuples)))
|
||||
}
|
||||
if len(newTuples) > 0 {
|
||||
hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "write").Add(float64(len(newTuples)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b.logger.Debug("no tuples to update in zanzana", "namespace", newRP.Namespace)
|
||||
}
|
||||
}()
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AfterResourcePermissionDelete is a post-delete hook that removes the resource permission from Zanzana (openFGA)
|
||||
func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionDelete(obj runtime.Object, _ *metav1.DeleteOptions) {
|
||||
if b.zClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rp, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
b.logger.Error("failed to convert object to resourcePermission type", "object", obj)
|
||||
return
|
||||
}
|
||||
|
||||
resourceType := "resourcepermission"
|
||||
operation := "delete"
|
||||
|
||||
// Grab a ticket to write to Zanzana
|
||||
// This limits the amount of concurrent connections to Zanzana
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
|
||||
go func(rp *iamv0.ResourcePermission) {
|
||||
start := time.Now()
|
||||
status := "success"
|
||||
|
||||
defer func() {
|
||||
// Release the ticket after write is done
|
||||
<-b.zTickets
|
||||
// Record operation duration and count
|
||||
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
|
||||
hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
|
||||
}()
|
||||
|
||||
resource := rp.Spec.Resource
|
||||
permissions := rp.Spec.Permissions
|
||||
|
||||
object := zanzana.NewObjectEntry(toZanzanaType(resource.ApiGroup), resource.ApiGroup, resource.Resource, "", resource.Name)
|
||||
|
||||
// Generate delete tuples from the permissions
|
||||
deleteTuples := make([]*v1.TupleKeyWithoutCondition, 0, len(permissions))
|
||||
for _, p := range permissions {
|
||||
tuple, err := NewResourceTuple(object, resource, p)
|
||||
if err != nil {
|
||||
b.logger.Error("failed to create resource permission tuple for deletion",
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
deleteTuples = append(deleteTuples, tupleToTupleKeyWithoutCondition(tuple))
|
||||
}
|
||||
|
||||
// Avoid writing if there are no valid tuples
|
||||
if len(deleteTuples) == 0 {
|
||||
b.logger.Warn("no valid tuples to delete", "namespace", rp.Namespace, "resource", object)
|
||||
status = "failure"
|
||||
return
|
||||
}
|
||||
|
||||
b.logger.Debug("deleting resource permission from zanzana",
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"tuplesCnt", len(deleteTuples),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := b.zClient.Write(ctx, &v1.WriteRequest{
|
||||
Namespace: rp.Namespace,
|
||||
Deletes: &v1.WriteRequestDeletes{
|
||||
TupleKeys: deleteTuples,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
status = "failure"
|
||||
b.logger.Error("failed to delete resource permission from zanzana",
|
||||
"err", err,
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"tuplesCnt", len(deleteTuples),
|
||||
)
|
||||
} else {
|
||||
// Record successful tuple deletions
|
||||
hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Add(float64(len(deleteTuples)))
|
||||
}
|
||||
}(rp.DeepCopy()) // Pass a copy of the object
|
||||
}
|
||||
@@ -251,7 +508,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object,
|
||||
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
|
||||
hooksWaitHistogram.WithLabelValues("role", "create").Observe(time.Since(wait).Seconds())
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
@@ -346,7 +603,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object,
|
||||
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
|
||||
hooksWaitHistogram.WithLabelValues(roleType, "delete").Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
@@ -499,15 +756,10 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context
|
||||
return
|
||||
}
|
||||
|
||||
// Prime the ticket channel if empty, then grab a ticket (receive) to avoid test hangs
|
||||
select {
|
||||
case b.zTickets <- true:
|
||||
default:
|
||||
}
|
||||
// Grab a ticket to write to Zanzana
|
||||
wait := time.Now()
|
||||
<-b.zTickets
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
|
||||
hooksWaitHistogram.WithLabelValues(roleType, "update").Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
|
||||
@@ -17,6 +17,7 @@ type FakeZanzanaClient struct {
|
||||
zanzana.Client
|
||||
readCallback func(context.Context, *v1.ReadRequest) (*v1.ReadResponse, error)
|
||||
writeCallback func(context.Context, *v1.WriteRequest) error
|
||||
readCallback func(context.Context, *v1.ReadRequest) (*v1.ReadResponse, error)
|
||||
}
|
||||
|
||||
// Read implements zanzana.Client.
|
||||
@@ -32,6 +33,14 @@ func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) err
|
||||
return f.writeCallback(ctx, req)
|
||||
}
|
||||
|
||||
// Read implements zanzana.Client.
|
||||
func (f *FakeZanzanaClient) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) {
|
||||
if f.readCallback != nil {
|
||||
return f.readCallback(ctx, req)
|
||||
}
|
||||
return &v1.ReadResponse{}, nil
|
||||
}
|
||||
|
||||
func requireTuplesMatch(t *testing.T, actual []*v1.TupleKey, expected []*v1.TupleKey, msgAndArgs ...interface{}) {
|
||||
t.Helper()
|
||||
for _, exp := range expected {
|
||||
@@ -164,6 +173,254 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBeginResourcePermissionUpdate(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
|
||||
t.Run("should update zanzana entries for folder resource permissions", func(t *testing.T) {
|
||||
oldFolderPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-2",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
newFolderPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-2",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u2", Verb: "Edit"},
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "View"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testFolderWrite := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
|
||||
// Should delete old permission
|
||||
require.NotNil(t, req.Deletes)
|
||||
require.Len(t, req.Deletes.TupleKeys, 1)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[0],
|
||||
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: "folder:fold1"},
|
||||
)
|
||||
|
||||
// Should write new permissions
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 2)
|
||||
|
||||
expectedWrites := []*v1.TupleKey{
|
||||
{User: "user:u2", Relation: "edit", Object: "folder:fold1"},
|
||||
{User: "team:team1#member", Relation: "view", Object: "folder:fold1"},
|
||||
}
|
||||
requireTuplesMatch(t, req.Writes.TupleKeys, expectedWrites)
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testFolderWrite}
|
||||
|
||||
// Call BeginUpdate which does all the work
|
||||
finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newFolderPerm, &oldFolderPerm, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, finishFunc)
|
||||
|
||||
// Call the finish function with success=true to trigger the zanzana write
|
||||
finishFunc(context.Background(), true)
|
||||
})
|
||||
|
||||
// Wait for the ticket to be released
|
||||
<-b.zTickets
|
||||
|
||||
t.Run("should update zanzana entries for dashboard resource permissions", func(t *testing.T) {
|
||||
oldDashPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
newDashPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "Edit"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
object := "resource:dashboard.grafana.app/dashboards/dash1"
|
||||
|
||||
testDashWrite := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "default", req.Namespace)
|
||||
|
||||
// Should delete old permission
|
||||
require.NotNil(t, req.Deletes)
|
||||
require.Len(t, req.Deletes.TupleKeys, 1)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[0],
|
||||
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: object},
|
||||
)
|
||||
|
||||
// Should write new permission
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 1)
|
||||
|
||||
tuple := req.Writes.TupleKeys[0]
|
||||
require.NotNil(t, tuple.Condition)
|
||||
require.Equal(t, "group_filter", tuple.Condition.Name)
|
||||
tuple.Condition = nil
|
||||
require.Equal(
|
||||
t,
|
||||
tuple,
|
||||
&v1.TupleKey{User: "service-account:sa1", Relation: "edit", Object: object},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testDashWrite}
|
||||
|
||||
// Call BeginUpdate which does all the work
|
||||
finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newDashPerm, &oldDashPerm, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, finishFunc)
|
||||
|
||||
// Call the finish function with success=true to trigger the zanzana write
|
||||
finishFunc(context.Background(), true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAfterResourcePermissionDelete(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
|
||||
t.Run("should delete zanzana entries for folder resource permissions", func(t *testing.T) {
|
||||
folderPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-2",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testFolderDelete := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
|
||||
// Should have deletes but no writes
|
||||
require.NotNil(t, req.Deletes)
|
||||
require.Len(t, req.Deletes.TupleKeys, 2)
|
||||
require.Nil(t, req.Writes)
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[0],
|
||||
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: "folder:fold1"},
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[1],
|
||||
&v1.TupleKeyWithoutCondition{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testFolderDelete}
|
||||
b.AfterResourcePermissionDelete(&folderPerm, nil)
|
||||
})
|
||||
|
||||
// Wait for the ticket to be released
|
||||
<-b.zTickets
|
||||
|
||||
t.Run("should delete zanzana entries for dashboard resource permissions", func(t *testing.T) {
|
||||
dashPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"},
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testDashDelete := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
object := "resource:dashboard.grafana.app/dashboards/dash1"
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "default", req.Namespace)
|
||||
|
||||
// Should have deletes but no writes
|
||||
require.NotNil(t, req.Deletes)
|
||||
require.Len(t, req.Deletes.TupleKeys, 2)
|
||||
require.Nil(t, req.Writes)
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[0],
|
||||
&v1.TupleKeyWithoutCondition{User: "service-account:sa1", Relation: "view", Object: object},
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Deletes.TupleKeys[1],
|
||||
&v1.TupleKeyWithoutCondition{User: "team:team1#member", Relation: "edit", Object: object},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testDashDelete}
|
||||
b.AfterResourcePermissionDelete(&dashPerm, nil)
|
||||
})
|
||||
|
||||
// Wait for the ticket to be released
|
||||
<-b.zTickets
|
||||
}
|
||||
|
||||
func TestAfterCoreRoleCreate(t *testing.T) {
|
||||
t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DELETE FROM {{ .Ident .TeamMemberTable }}
|
||||
WHERE uid = {{ .Arg .Command.UID }}
|
||||
@@ -37,6 +37,8 @@ type LegacyIdentityStore interface {
|
||||
CreateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamMemberCommand) (*CreateTeamMemberResult, error)
|
||||
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
|
||||
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
|
||||
UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error)
|
||||
DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error
|
||||
}
|
||||
|
||||
var _ LegacyIdentityStore = (*legacySQLStore)(nil)
|
||||
|
||||
@@ -85,12 +85,24 @@ func TestIdentityQueries(t *testing.T) {
|
||||
return &v
|
||||
}
|
||||
|
||||
updateTeamMember := func(cmd *UpdateTeamMemberCommand) sqltemplate.SQLTemplate {
|
||||
v := newUpdateTeamMember(nodb, cmd)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
listTeamMembers := func(q *ListTeamMembersQuery) sqltemplate.SQLTemplate {
|
||||
v := newListTeamMembers(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
deleteTeamMember := func(q *DeleteTeamMemberCommand) sqltemplate.SQLTemplate {
|
||||
v := newDeleteTeamMember(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
deleteTeam := func(q *DeleteTeamCommand) sqltemplate.SQLTemplate {
|
||||
v := newDeleteTeam(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
@@ -260,6 +272,16 @@ func TestIdentityQueries(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlUpdateTeamMemberQuery: {
|
||||
{
|
||||
Name: "update_team_member_basic",
|
||||
Data: updateTeamMember(&UpdateTeamMemberCommand{
|
||||
UID: "team-member-1",
|
||||
Permission: team.PermissionTypeAdmin,
|
||||
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlQueryTeamMembersTemplate: {
|
||||
{
|
||||
Name: "team_1_members_page_1",
|
||||
@@ -278,6 +300,14 @@ func TestIdentityQueries(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlDeleteTeamMemberQuery: {
|
||||
{
|
||||
Name: "delete_team_member_basic",
|
||||
Data: deleteTeamMember(&DeleteTeamMemberCommand{
|
||||
UID: "team-member-1",
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlQueryUserTeamsTemplate: {
|
||||
{
|
||||
Name: "team_1_members_page_1",
|
||||
|
||||
@@ -301,6 +301,128 @@ func (s *legacySQLStore) ListTeamMembers(ctx context.Context, ns claims.Namespac
|
||||
return res, err
|
||||
}
|
||||
|
||||
type UpdateTeamMemberCommand struct {
|
||||
UID string
|
||||
Permission team.PermissionType
|
||||
Updated legacysql.DBTime
|
||||
}
|
||||
|
||||
type UpdateTeamMemberResult struct {
|
||||
UID string
|
||||
Permission team.PermissionType
|
||||
Updated legacysql.DBTime
|
||||
}
|
||||
|
||||
var sqlUpdateTeamMemberQuery = mustTemplate("update_team_member_query.sql")
|
||||
|
||||
func newUpdateTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *UpdateTeamMemberCommand) updateTeamMemberQuery {
|
||||
return updateTeamMemberQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
TeamMemberTable: sql.Table("team_member"),
|
||||
Command: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
type updateTeamMemberQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
TeamMemberTable string
|
||||
Command *UpdateTeamMemberCommand
|
||||
}
|
||||
|
||||
func (r updateTeamMemberQuery) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacySQLStore) UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error) {
|
||||
now := time.Now().UTC()
|
||||
cmd.Updated = legacysql.NewDBTime(now)
|
||||
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := newUpdateTeamMember(sql, &cmd)
|
||||
|
||||
var result UpdateTeamMemberResult
|
||||
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
|
||||
teamMemberQuery, err := sqltemplate.Execute(sqlUpdateTeamMemberQuery, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute team member template %q: %w", sqlUpdateTeamMemberQuery.Name(), err)
|
||||
}
|
||||
|
||||
_, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update team member: %w", err)
|
||||
}
|
||||
|
||||
result = UpdateTeamMemberResult(cmd)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
type DeleteTeamMemberCommand struct {
|
||||
UID string
|
||||
}
|
||||
|
||||
var sqlDeleteTeamMemberQuery = mustTemplate("delete_team_member_query.sql")
|
||||
|
||||
func newDeleteTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *DeleteTeamMemberCommand) deleteTeamMemberQuery {
|
||||
return deleteTeamMemberQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
TeamMemberTable: sql.Table("team_member"),
|
||||
Command: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
type deleteTeamMemberQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
TeamMemberTable string
|
||||
Command *DeleteTeamMemberCommand
|
||||
}
|
||||
|
||||
func (r deleteTeamMemberQuery) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacySQLStore) DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error {
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newDeleteTeamMember(sql, &cmd)
|
||||
if err := req.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
|
||||
teamMemberQuery, err := sqltemplate.Execute(sqlDeleteTeamMemberQuery, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute team member template %q: %w", sqlDeleteTeamMemberQuery.Name(), err)
|
||||
}
|
||||
|
||||
_, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete team member: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanMember(rows *sql.Rows) (TeamMember, error) {
|
||||
m := TeamMember{}
|
||||
err := rows.Scan(&m.ID, &m.UID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission)
|
||||
|
||||
Vendored
Executable
+2
@@ -0,0 +1,2 @@
|
||||
DELETE FROM `grafana`.`team_member`
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
Executable
+4
@@ -0,0 +1,4 @@
|
||||
UPDATE `grafana`.`team_member`
|
||||
SET permission = 'Admin',
|
||||
updated = '2023-01-01 12:00:00'
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
Executable
+2
@@ -0,0 +1,2 @@
|
||||
DELETE FROM "grafana"."team_member"
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
Executable
+4
@@ -0,0 +1,4 @@
|
||||
UPDATE "grafana"."team_member"
|
||||
SET permission = 'Admin',
|
||||
updated = '2023-01-01 12:00:00'
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
Executable
+2
@@ -0,0 +1,2 @@
|
||||
DELETE FROM "grafana"."team_member"
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
Executable
+4
@@ -0,0 +1,4 @@
|
||||
UPDATE "grafana"."team_member"
|
||||
SET permission = 'Admin',
|
||||
updated = '2023-01-01 12:00:00'
|
||||
WHERE uid = 'team-member-1'
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE {{ .Ident .TeamMemberTable }}
|
||||
SET permission = {{ .Arg .Command.Permission }},
|
||||
updated = {{ .Arg .Command.Updated }}
|
||||
WHERE uid = {{ .Arg .Command.UID }}
|
||||
@@ -14,19 +14,53 @@ const (
|
||||
|
||||
var (
|
||||
registerOnce sync.Once
|
||||
hooksWaitHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
hooksWaitHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
Name: "hooks_wait_duration_seconds",
|
||||
Help: "Time spent in the hooks waiting for a ticket to start processing",
|
||||
Buckets: prometheus.ExponentialBuckets(0.001, 2, 5), // 1ms to ~16s
|
||||
})
|
||||
}, []string{"resource_type", "operation"})
|
||||
|
||||
// hooksDurationHistogram tracks the total duration of hook operations
|
||||
hooksDurationHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
Name: "hooks_operation_duration_seconds",
|
||||
Help: "Time spent executing hook operations (create, update, delete)",
|
||||
Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), // 1ms to ~1s
|
||||
}, []string{"resource_type", "operation", "status"})
|
||||
|
||||
// hooksOperationCounter tracks the number of hook operations
|
||||
hooksOperationCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
Name: "hooks_operations_total",
|
||||
Help: "Total number of hook operations by resource type, operation, and status",
|
||||
}, []string{"resource_type", "operation", "status"})
|
||||
|
||||
// hooksTuplesCounter tracks the number of tuples written/deleted
|
||||
hooksTuplesCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
Name: "hooks_tuples_total",
|
||||
Help: "Total number of tuples written or deleted by resource type and operation type",
|
||||
}, []string{"resource_type", "operation", "action"})
|
||||
)
|
||||
|
||||
func registerMetrics(reg prometheus.Registerer) {
|
||||
registerOnce.Do(func() {
|
||||
if err := reg.Register(hooksWaitHistogram); err != nil {
|
||||
log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err)
|
||||
metrics := []prometheus.Collector{
|
||||
hooksWaitHistogram,
|
||||
hooksDurationHistogram,
|
||||
hooksOperationCounter,
|
||||
hooksTuplesCounter,
|
||||
}
|
||||
|
||||
for _, metric := range metrics {
|
||||
if err := reg.Register(metric); err != nil {
|
||||
log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
|
||||
@@ -59,6 +61,10 @@ type IdentityAccessManagementAPIBuilder struct {
|
||||
reg prometheus.Registerer
|
||||
logger log.Logger
|
||||
|
||||
dual dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
userSearchClient resourcepb.ResourceIndexClient
|
||||
|
||||
// non-k8s api route
|
||||
display *user.LegacyDisplayREST
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
legacyuser "github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -61,6 +63,9 @@ func RegisterAPIService(
|
||||
coreRolesStorage CoreRoleStorageBackend,
|
||||
rolesStorage RoleStorageBackend,
|
||||
roleBindingsStorage RoleBindingStorageBackend,
|
||||
dual dualwrite.Service,
|
||||
unified resource.ResourceClient,
|
||||
userService legacyuser.Service,
|
||||
) (*IdentityAccessManagementAPIBuilder, error) {
|
||||
dbProvider := legacysql.NewDatabaseProvider(sql)
|
||||
store := legacy.NewLegacySQLStores(dbProvider)
|
||||
@@ -85,6 +90,9 @@ func RegisterAPIService(
|
||||
logger: log.New("iam.apis"),
|
||||
features: features,
|
||||
enableDualWriter: true,
|
||||
dual: dual,
|
||||
unified: unified,
|
||||
userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features),
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
|
||||
@@ -130,11 +138,13 @@ func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersi
|
||||
}
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
|
||||
if err := iamv0.AddAuthZKnownTypes(scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
|
||||
if err := iamv0.AddResourcePermissionKnownTypes(scheme, iamv0.SchemeGroupVersion); err != nil {
|
||||
return err
|
||||
@@ -162,7 +172,9 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
storage := map[string]rest.Storage{}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync)
|
||||
|
||||
// teams + users must have shorter names because they are often used as part of another name
|
||||
@@ -257,6 +269,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
|
||||
// v0alpha1
|
||||
coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
|
||||
@@ -289,15 +302,17 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
}
|
||||
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
|
||||
resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enableZanzanaSync {
|
||||
b.logger.Info("Enabling AfterCreate hook for ResourcePermission to sync to Zanzana")
|
||||
b.logger.Info("Enabling AfterCreate, BeginUpdate, and AfterDelete hooks for ResourcePermission to sync to Zanzana")
|
||||
resourcePermissionStore.AfterCreate = b.AfterResourcePermissionCreate
|
||||
resourcePermissionStore.BeginUpdate = b.BeginResourcePermissionUpdate
|
||||
resourcePermissionStore.AfterDelete = b.AfterResourcePermissionDelete
|
||||
}
|
||||
storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore
|
||||
}
|
||||
@@ -384,7 +399,7 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
|
||||
case admission.Create:
|
||||
switch typedObj := a.GetObject().(type) {
|
||||
case *iamv0.User:
|
||||
return user.ValidateOnCreate(ctx, typedObj)
|
||||
return user.ValidateOnCreate(ctx, b.userSearchClient, typedObj)
|
||||
case *iamv0.ServiceAccount:
|
||||
return serviceaccount.ValidateOnCreate(ctx, typedObj)
|
||||
case *iamv0.Team:
|
||||
@@ -402,7 +417,7 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
|
||||
if !ok {
|
||||
return fmt.Errorf("expected old object to be a User, got %T", oldUserObj)
|
||||
}
|
||||
return user.ValidateOnUpdate(ctx, oldUserObj, typedObj)
|
||||
return user.ValidateOnUpdate(ctx, b.userSearchClient, oldUserObj, typedObj)
|
||||
case *iamv0.ResourcePermission:
|
||||
return resourcepermission.ValidateCreateAndUpdateInput(ctx, typedObj)
|
||||
case *iamv0.Team:
|
||||
@@ -411,6 +426,12 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
|
||||
return fmt.Errorf("expected old object to be a Team, got %T", oldTeamObj)
|
||||
}
|
||||
return team.ValidateOnUpdate(ctx, typedObj, oldTeamObj)
|
||||
case *iamv0.TeamBinding:
|
||||
oldTeamBindingObj, ok := a.GetOldObject().(*iamv0.TeamBinding)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected old object to be a TeamBinding, got %T", oldTeamBindingObj)
|
||||
}
|
||||
return teambinding.ValidateOnUpdate(ctx, typedObj, oldTeamBindingObj)
|
||||
}
|
||||
return nil
|
||||
case admission.Delete:
|
||||
|
||||
@@ -73,11 +73,86 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime.
|
||||
}
|
||||
|
||||
func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update")
|
||||
if !l.enableAuthnMutation {
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update")
|
||||
}
|
||||
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
oldObj, err := l.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return oldObj, false, err
|
||||
}
|
||||
|
||||
obj, err := objInfo.UpdatedObject(ctx, oldObj)
|
||||
if err != nil {
|
||||
return oldObj, false, err
|
||||
}
|
||||
|
||||
teamBindingObj, ok := obj.(*iamv0alpha1.TeamBinding)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("expected TeamBinding object, got %T", obj)
|
||||
}
|
||||
|
||||
if updateValidation != nil {
|
||||
if err := updateValidation(ctx, obj, oldObj); err != nil {
|
||||
return oldObj, false, err
|
||||
}
|
||||
}
|
||||
|
||||
var permission team.PermissionType
|
||||
switch teamBindingObj.Spec.Permission {
|
||||
case iamv0alpha1.TeamBindingTeamPermissionAdmin:
|
||||
permission = team.PermissionTypeAdmin
|
||||
case iamv0alpha1.TeamBindingTeamPermissionMember:
|
||||
permission = team.PermissionTypeMember
|
||||
}
|
||||
|
||||
updateCmd := legacy.UpdateTeamMemberCommand{
|
||||
UID: teamBindingObj.Name,
|
||||
Permission: permission,
|
||||
}
|
||||
|
||||
_, err = l.store.UpdateTeamMember(ctx, ns, updateCmd)
|
||||
if err != nil {
|
||||
return oldObj, false, err
|
||||
}
|
||||
|
||||
return teamBindingObj, false, nil
|
||||
}
|
||||
|
||||
func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
|
||||
if !l.enableAuthnMutation {
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Check if the team binding exists
|
||||
_, err = l.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
err = l.store.DeleteTeamMember(ctx, ns, legacy.DeleteTeamMemberCommand{
|
||||
UID: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return &iamv0alpha1.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns.Value,
|
||||
},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
|
||||
@@ -29,3 +29,28 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.TeamBinding) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateOnUpdate(ctx context.Context, obj, old *iamv0alpha1.TeamBinding) error {
|
||||
_, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
}
|
||||
|
||||
if obj.Spec.TeamRef.Name != old.Spec.TeamRef.Name {
|
||||
return apierrors.NewBadRequest("teamRef is immutable")
|
||||
}
|
||||
|
||||
if obj.Spec.Subject.Name != old.Spec.Subject.Name {
|
||||
return apierrors.NewBadRequest("subject is immutable")
|
||||
}
|
||||
|
||||
if obj.Spec.External != old.Spec.External {
|
||||
return apierrors.NewBadRequest("external is immutable")
|
||||
}
|
||||
|
||||
if obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionAdmin && obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionMember {
|
||||
return apierrors.NewBadRequest("invalid permission")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,3 +121,242 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOnUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requester *identity.StaticRequester
|
||||
old *iamv0alpha1.TeamBinding
|
||||
obj *iamv0alpha1.TeamBinding
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "valid update - permission change",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionMember,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "valid update - no changes",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid update - teamRef change",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team-updated",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
want: apierrors.NewBadRequest("teamRef is immutable"),
|
||||
},
|
||||
{
|
||||
name: "invalid update - subject change",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user-updated",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
want: apierrors.NewBadRequest("subject is immutable"),
|
||||
},
|
||||
{
|
||||
name: "invalid update - external change",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: true,
|
||||
},
|
||||
},
|
||||
want: apierrors.NewBadRequest("external is immutable"),
|
||||
},
|
||||
{
|
||||
name: "invalid update - invalid permission",
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: "invalid",
|
||||
},
|
||||
},
|
||||
want: apierrors.NewBadRequest("invalid permission"),
|
||||
},
|
||||
{
|
||||
name: "invalid update - no requester in context",
|
||||
requester: nil,
|
||||
old: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
obj: &iamv0alpha1.TeamBinding{
|
||||
Spec: iamv0alpha1.TeamBindingSpec{
|
||||
Subject: iamv0alpha1.TeamBindingspecSubject{
|
||||
Name: "test-user",
|
||||
},
|
||||
TeamRef: iamv0alpha1.TeamBindingTeamRef{
|
||||
Name: "test-team",
|
||||
},
|
||||
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
|
||||
External: false,
|
||||
},
|
||||
},
|
||||
want: apierrors.NewUnauthorized("no identity found"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := identity.WithRequester(context.Background(), test.requester)
|
||||
err := ValidateOnUpdate(ctx, test.obj, test.old)
|
||||
assert.Equal(t, test.want, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
res "github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
)
|
||||
|
||||
const (
|
||||
UserResource = "users"
|
||||
UserResourceGroup = "iam.grafana.com"
|
||||
)
|
||||
|
||||
// UserLegacySearchClient is a client for searching for users in the legacy search engine.
|
||||
type UserLegacySearchClient struct {
|
||||
resourcepb.ResourceIndexClient
|
||||
userService user.Service
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewUserLegacySearchClient creates a new UserLegacySearchClient.
|
||||
func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient {
|
||||
return &UserLegacySearchClient{
|
||||
userService: userService,
|
||||
log: slog.Default().With("logger", "legacy-user-search-client"),
|
||||
}
|
||||
}
|
||||
|
||||
// Search searches for users in the legacy search engine.
|
||||
// It only supports exact matching for title, login, or email.
|
||||
// FIXME: This implementation only supports a single field query and will be extended in the future.
|
||||
func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
|
||||
signedInUser, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Limit > 100 {
|
||||
req.Limit = 100
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 1
|
||||
}
|
||||
|
||||
if req.Page > math.MaxInt32 || req.Page < 0 {
|
||||
return nil, fmt.Errorf("invalid page number: %d", req.Page)
|
||||
}
|
||||
|
||||
query := &user.SearchUsersQuery{
|
||||
SignedInUser: signedInUser,
|
||||
Limit: int(req.Limit),
|
||||
Page: int(req.Page),
|
||||
}
|
||||
|
||||
var title, login, email string
|
||||
for _, field := range req.Options.Fields {
|
||||
vals := field.GetValues()
|
||||
if len(vals) != 1 {
|
||||
c.log.Warn("only single value fields are supported for legacy search, using first value", "field", field.Key, "values", vals)
|
||||
}
|
||||
switch field.Key {
|
||||
case res.SEARCH_FIELD_TITLE:
|
||||
title = vals[0]
|
||||
case "fields.login":
|
||||
login = vals[0]
|
||||
case "fields.email":
|
||||
email = vals[0]
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" && login == "" && email == "" {
|
||||
return nil, fmt.Errorf("at least one of title, login, or email must be provided for the query")
|
||||
}
|
||||
|
||||
// The user store's Search method combines these into an OR.
|
||||
// For legacy search we can only supply one.
|
||||
if title != "" {
|
||||
query.Query = title
|
||||
} else if login != "" {
|
||||
query.Query = login
|
||||
} else {
|
||||
query.Query = email
|
||||
}
|
||||
|
||||
columns := getColumns(req.Fields)
|
||||
list := &resourcepb.ResourceSearchResponse{
|
||||
Results: &resourcepb.ResourceTable{
|
||||
Columns: columns,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := c.userService.Search(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, u := range res.Users {
|
||||
cells := createBaseCells(u, req.Fields)
|
||||
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
|
||||
Key: getResourceKey(u, req.Options.Key.Namespace),
|
||||
Cells: cells,
|
||||
})
|
||||
}
|
||||
|
||||
list.TotalHits = res.TotalCount
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func getResourceKey(item *user.UserSearchHitDTO, namespace string) *resourcepb.ResourceKey {
|
||||
return &resourcepb.ResourceKey{
|
||||
Namespace: namespace,
|
||||
Group: UserResourceGroup,
|
||||
Resource: UserResource,
|
||||
Name: item.UID,
|
||||
}
|
||||
}
|
||||
|
||||
func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition {
|
||||
columns := defaultColumns()
|
||||
for _, field := range fields {
|
||||
switch field {
|
||||
case "email":
|
||||
columns = append(columns, search.TableColumnDefinitions[search.USER_EMAIL])
|
||||
case "login":
|
||||
columns = append(columns, search.TableColumnDefinitions[search.USER_LOGIN])
|
||||
}
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
func createBaseCells(u *user.UserSearchHitDTO, fields []string) [][]byte {
|
||||
cells := createDefaultCells(u)
|
||||
for _, field := range fields {
|
||||
switch field {
|
||||
case "email":
|
||||
cells = append(cells, []byte(u.Email))
|
||||
case "login":
|
||||
cells = append(cells, []byte(u.Login))
|
||||
}
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
func createDefaultCells(u *user.UserSearchHitDTO) [][]byte {
|
||||
return [][]byte{
|
||||
[]byte(u.UID),
|
||||
[]byte(u.Name),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultColumns() []*resourcepb.ResourceTableColumnDefinition {
|
||||
searchFields := res.StandardSearchFields()
|
||||
return []*resourcepb.ResourceTableColumnDefinition{
|
||||
searchFields.Field(res.SEARCH_FIELD_NAME),
|
||||
searchFields.Field(res.SEARCH_FIELD_TITLE),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
// FakeUserLegacySearchClient is a fake implementation of UserLegacySearchClient for testing.
|
||||
type FakeUserLegacySearchClient struct {
|
||||
resourcepb.ResourceIndexClient
|
||||
SearchFunc func(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error)
|
||||
Users []*user.UserSearchHitDTO
|
||||
}
|
||||
|
||||
// Search calls the underlying SearchFunc or simulates a search over the Users slice.
|
||||
func (c *FakeUserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
|
||||
if c.SearchFunc != nil {
|
||||
return c.SearchFunc(ctx, req, opts...)
|
||||
}
|
||||
|
||||
// Basic filtering for testing purposes
|
||||
var filteredUsers []*user.UserSearchHitDTO
|
||||
var queryValue string
|
||||
|
||||
for _, field := range req.Options.Fields {
|
||||
if len(field.Values) > 0 {
|
||||
queryValue = field.Values[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range c.Users {
|
||||
if u.Login == queryValue || u.Email == queryValue {
|
||||
filteredUsers = append(filteredUsers, u)
|
||||
}
|
||||
}
|
||||
|
||||
rows := make([]*resourcepb.ResourceTableRow, 0, len(filteredUsers))
|
||||
for _, u := range filteredUsers {
|
||||
rows = append(rows, &resourcepb.ResourceTableRow{
|
||||
Key: getResourceKey(u, req.Options.Key.Namespace),
|
||||
Cells: createBaseCells(u, req.Fields),
|
||||
})
|
||||
}
|
||||
|
||||
return &resourcepb.ResourceSearchResponse{
|
||||
Results: &resourcepb.ResourceTable{
|
||||
Columns: getColumns(req.Fields),
|
||||
Rows: rows,
|
||||
},
|
||||
TotalHits: int64(len(filteredUsers)),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
res "github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
func TestUserLegacySearchClient_Search(t *testing.T) {
|
||||
t.Run("should return error if no query fields are provided", func(t *testing.T) {
|
||||
mockUserService := usertest.NewMockService(t)
|
||||
client := NewUserLegacySearchClient(mockUserService)
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
|
||||
req := &resourcepb.ResourceSearchRequest{
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: &resourcepb.ResourceKey{Namespace: "default"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.Search(ctx, req)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "at least one of title, login, or email must be provided for the query", err.Error())
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
fieldKey string
|
||||
fieldValues []string
|
||||
expectedQuery string
|
||||
}{
|
||||
{
|
||||
name: "search by title",
|
||||
fieldKey: res.SEARCH_FIELD_TITLE,
|
||||
fieldValues: []string{"test user"},
|
||||
expectedQuery: "test user",
|
||||
},
|
||||
{
|
||||
name: "search by title (multiple values)",
|
||||
fieldKey: res.SEARCH_FIELD_TITLE,
|
||||
fieldValues: []string{"user1", "user2"},
|
||||
expectedQuery: "user1",
|
||||
},
|
||||
{
|
||||
name: "search by login",
|
||||
fieldKey: "fields.login",
|
||||
fieldValues: []string{"testlogin"},
|
||||
expectedQuery: "testlogin",
|
||||
},
|
||||
{
|
||||
name: "search by email",
|
||||
fieldKey: "fields.email",
|
||||
fieldValues: []string{"test@example.com"},
|
||||
expectedQuery: "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mockUserService := usertest.NewMockService(t)
|
||||
client := NewUserLegacySearchClient(mockUserService)
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
|
||||
req := &resourcepb.ResourceSearchRequest{
|
||||
Limit: 10,
|
||||
Page: 1,
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: &resourcepb.ResourceKey{Namespace: "default"},
|
||||
Fields: []*resourcepb.Requirement{
|
||||
{Key: tc.fieldKey, Values: tc.fieldValues},
|
||||
},
|
||||
},
|
||||
Fields: []string{"email", "login"},
|
||||
}
|
||||
|
||||
mockUsers := []*user.UserSearchHitDTO{
|
||||
{ID: 1, UID: "uid1", Name: "Test User 1", Email: "test1@example.com", Login: "testlogin1"},
|
||||
}
|
||||
|
||||
mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool {
|
||||
return q.Query == tc.expectedQuery && q.Limit == 10 && q.Page == 1
|
||||
})).Return(&user.SearchUserQueryResult{
|
||||
Users: mockUsers,
|
||||
TotalCount: 1,
|
||||
}, nil)
|
||||
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, int64(1), resp.TotalHits)
|
||||
require.Len(t, resp.Results.Rows, 1)
|
||||
|
||||
// Verify columns
|
||||
expectedColumns := getColumns(req.Fields)
|
||||
require.Equal(t, len(expectedColumns), len(resp.Results.Columns))
|
||||
for i, col := range resp.Results.Columns {
|
||||
require.Equal(t, expectedColumns[i].Name, col.Name)
|
||||
}
|
||||
|
||||
// Verify rows
|
||||
for i, u := range mockUsers {
|
||||
row := resp.Results.Rows[i]
|
||||
require.Equal(t, "default", row.Key.Namespace)
|
||||
require.Equal(t, UserResourceGroup, row.Key.Group)
|
||||
require.Equal(t, UserResource, row.Key.Resource)
|
||||
require.Equal(t, u.UID, row.Key.Name)
|
||||
|
||||
expectedCells := createBaseCells(&user.UserSearchHitDTO{
|
||||
UID: u.UID,
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
Login: u.Login,
|
||||
}, req.Fields)
|
||||
require.Equal(t, expectedCells, row.Cells)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("title should have precedence over login and email", func(t *testing.T) {
|
||||
mockUserService := usertest.NewMockService(t)
|
||||
client := NewUserLegacySearchClient(mockUserService)
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
|
||||
req := &resourcepb.ResourceSearchRequest{
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: &resourcepb.ResourceKey{Namespace: "default"},
|
||||
Fields: []*resourcepb.Requirement{
|
||||
{Key: res.SEARCH_FIELD_TITLE, Values: []string{"title"}},
|
||||
{Key: "fields.login", Values: []string{"login"}},
|
||||
{Key: "fields.email", Values: []string{"email"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool {
|
||||
return q.Query == "title"
|
||||
})).Return(&user.SearchUserQueryResult{Users: []*user.UserSearchHitDTO{}, TotalCount: 0}, nil)
|
||||
|
||||
_, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda
|
||||
UID: name,
|
||||
Login: userObj.Spec.Login,
|
||||
Email: userObj.Spec.Email,
|
||||
Name: userObj.Spec.Name,
|
||||
Name: userObj.Spec.Title,
|
||||
IsAdmin: userObj.Spec.GrafanaAdmin,
|
||||
IsDisabled: userObj.Spec.Disabled,
|
||||
EmailVerified: userObj.Spec.EmailVerified,
|
||||
@@ -258,7 +258,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali
|
||||
UID: userObj.Name,
|
||||
Login: userObj.Spec.Login,
|
||||
Email: userObj.Spec.Email,
|
||||
Name: userObj.Spec.Name,
|
||||
Name: userObj.Spec.Title,
|
||||
IsAdmin: userObj.Spec.GrafanaAdmin,
|
||||
IsDisabled: userObj.Spec.Disabled,
|
||||
EmailVerified: userObj.Spec.EmailVerified,
|
||||
@@ -284,7 +284,7 @@ func toUserItem(u *common.UserWithRole, ns string) iamv0alpha1.User {
|
||||
CreationTimestamp: metav1.NewTime(u.Created),
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Name: u.Name,
|
||||
Title: u.Name,
|
||||
Login: u.Login,
|
||||
Email: u.Email,
|
||||
EmailVerified: u.EmailVerified,
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
|
||||
func ValidateOnCreate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, obj *iamv0alpha1.User) error {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
@@ -28,27 +30,22 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
|
||||
return apierrors.NewBadRequest("user must have either login or email")
|
||||
}
|
||||
|
||||
err = validateRole(obj)
|
||||
if err != nil {
|
||||
if err := validateRole(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Login); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRole(obj *iamv0alpha1.User) error {
|
||||
if obj.Spec.Role == "" {
|
||||
return apierrors.NewBadRequest("role is required")
|
||||
}
|
||||
|
||||
if !identity.RoleType(obj.Spec.Role).IsValid() {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) error {
|
||||
func ValidateOnUpdate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, oldObj, newObj *iamv0alpha1.User) error {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
@@ -93,10 +90,109 @@ func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) err
|
||||
return apierrors.NewBadRequest("user must have either login or email")
|
||||
}
|
||||
|
||||
err = validateRole(newObj)
|
||||
if err != nil {
|
||||
if err := validateRole(newObj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newObj.Spec.Email != oldObj.Spec.Email {
|
||||
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newObj.Spec.Login != oldObj.Spec.Login {
|
||||
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Login); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRole(obj *iamv0alpha1.User) error {
|
||||
if obj.Spec.Role == "" {
|
||||
return apierrors.NewBadRequest("role is required")
|
||||
}
|
||||
|
||||
if !identity.RoleType(obj.Spec.Role).IsValid() {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEmail(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, email string) error {
|
||||
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
|
||||
{
|
||||
Key: "fields.email",
|
||||
Operator: string(selection.Equals),
|
||||
Values: []string{email},
|
||||
},
|
||||
}, []string{"name", "email", "login"})
|
||||
|
||||
resp, err := searchClient.Search(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(mgyongyosi): Improve the exact match validation
|
||||
|
||||
if resp.TotalHits > 0 {
|
||||
// If the found user is the same as the one being created/updated, it's not a conflict.
|
||||
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
|
||||
rows := resp.Results.Rows
|
||||
if len(rows) > 0 && rows[0].Key.Name == name {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
|
||||
name,
|
||||
fmt.Errorf("email '%s' is already taken", email))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLogin(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, login string) error {
|
||||
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
|
||||
{
|
||||
Key: "fields.login",
|
||||
Operator: string(selection.Equals),
|
||||
Values: []string{login},
|
||||
},
|
||||
}, []string{"name", "email", "login"})
|
||||
resp, err := searchClient.Search(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(mgyongyosi): Improve the exact match validation
|
||||
|
||||
if resp.TotalHits > 0 {
|
||||
// If the found user is the same as the one being created/updated, it's not a conflict.
|
||||
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
|
||||
rows := resp.Results.Rows
|
||||
if len(rows) > 0 && rows[0].Key.Name == name {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
|
||||
name,
|
||||
fmt.Errorf("login '%s' is already taken", login))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUserSearchRequest(namespace string, requirements []*resourcepb.Requirement, fields []string) *resourcepb.ResourceSearchRequest {
|
||||
userGvr := iamv0alpha1.UserResourceInfo.GroupResource()
|
||||
return &resourcepb.ResourceSearchRequest{
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Group: userGvr.Group,
|
||||
Resource: userGvr.Resource,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Fields: requirements,
|
||||
},
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestValidateOnCreate(t *testing.T) {
|
||||
@@ -16,6 +19,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
name string
|
||||
user *iamv0alpha1.User
|
||||
requester *identity.StaticRequester
|
||||
searchClient resourcepb.ResourceIndexClient
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
@@ -31,7 +35,8 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "grafana admin creating another grafana admin",
|
||||
@@ -46,7 +51,8 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-admin trying to create a grafana admin",
|
||||
@@ -61,6 +67,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: true,
|
||||
errorContains: "only grafana admins can create grafana admins",
|
||||
},
|
||||
@@ -75,6 +82,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: true,
|
||||
errorContains: "user must have either login or email",
|
||||
},
|
||||
@@ -90,13 +98,14 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "user with only email",
|
||||
user: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Email: "test@test.com",
|
||||
Email: "test@example",
|
||||
Role: "Viewer",
|
||||
},
|
||||
},
|
||||
@@ -104,7 +113,8 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "user with empty role",
|
||||
@@ -117,6 +127,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: true,
|
||||
errorContains: "role is required",
|
||||
},
|
||||
@@ -132,6 +143,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: true,
|
||||
errorContains: "invalid role 'InvalidRole'",
|
||||
},
|
||||
@@ -147,7 +159,55 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "user with existing email",
|
||||
user: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Email: "existing@example",
|
||||
Role: "Viewer",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{
|
||||
Users: []*user.UserSearchHitDTO{
|
||||
{Email: "existing@example"},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "email 'existing@example' is already taken",
|
||||
},
|
||||
{
|
||||
name: "user with existing login",
|
||||
user: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Login: "existinguser",
|
||||
Email: "existinguser@example",
|
||||
Role: "Viewer",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: false,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{
|
||||
Users: []*user.UserSearchHitDTO{
|
||||
{Login: "existinguser"},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "login 'existinguser' is already taken",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -158,7 +218,7 @@ func TestValidateOnCreate(t *testing.T) {
|
||||
tt.requester,
|
||||
)
|
||||
|
||||
err := ValidateOnCreate(ctx, tt.user)
|
||||
err := ValidateOnCreate(ctx, tt.searchClient, tt.user)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
@@ -178,6 +238,7 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
oldUser *iamv0alpha1.User
|
||||
newUser *iamv0alpha1.User
|
||||
requester *identity.StaticRequester
|
||||
searchClient resourcepb.ResourceIndexClient
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
@@ -254,7 +315,7 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
{
|
||||
name: "update with only login",
|
||||
oldUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{Email: "test@test.com", Role: "Viewer"},
|
||||
Spec: iamv0alpha1.UserSpec{Email: "test@example", Role: "Viewer"},
|
||||
},
|
||||
newUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "", Role: "Viewer"},
|
||||
@@ -263,7 +324,8 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "update with only email",
|
||||
@@ -271,13 +333,14 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
|
||||
},
|
||||
newUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{Login: "", Email: "test@test.com", Role: "Viewer"},
|
||||
Spec: iamv0alpha1.UserSpec{Login: "", Email: "test@example", Role: "Viewer"},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
expectError: false,
|
||||
searchClient: &FakeUserLegacySearchClient{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "service user verifies email",
|
||||
@@ -408,6 +471,77 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "update with existing email",
|
||||
oldUser: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{Email: "one@example", Role: "Viewer"},
|
||||
},
|
||||
newUser: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{Email: "two@example", Role: "Viewer"},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{
|
||||
Users: []*user.UserSearchHitDTO{
|
||||
{Email: "two@example"},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "email 'two@example' is already taken",
|
||||
},
|
||||
{
|
||||
name: "update with existing login",
|
||||
oldUser: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{Login: "one", Role: "Viewer"},
|
||||
},
|
||||
newUser: &iamv0alpha1.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userx",
|
||||
},
|
||||
Spec: iamv0alpha1.UserSpec{Login: "two", Role: "Viewer"},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{
|
||||
Users: []*user.UserSearchHitDTO{
|
||||
{Name: "other", UID: "uid456", Login: "two"},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "login 'two' is already taken",
|
||||
},
|
||||
{
|
||||
name: "update with no change to login or email",
|
||||
oldUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Viewer"},
|
||||
},
|
||||
newUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Editor"},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
IsGrafanaAdmin: true,
|
||||
},
|
||||
searchClient: &FakeUserLegacySearchClient{
|
||||
Users: []*user.UserSearchHitDTO{
|
||||
{Login: "testuser", Email: "test@example"},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -417,7 +551,7 @@ func TestValidateOnUpdate(t *testing.T) {
|
||||
tt.requester,
|
||||
)
|
||||
|
||||
err := ValidateOnUpdate(ctx, tt.oldUser, tt.newUser)
|
||||
err := ValidateOnUpdate(ctx, tt.searchClient, tt.oldUser, tt.newUser)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalAllFlags")
|
||||
ctx, span := tracing.Start(ctx, "ofrep.proxy.evalAllFlags")
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
@@ -70,7 +70,7 @@ func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w h
|
||||
}
|
||||
|
||||
func (b *APIBuilder) proxyFlagReq(ctx context.Context, flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalFlag")
|
||||
ctx, span := tracing.Start(ctx, "ofrep.proxy.evalFlag")
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -35,8 +34,6 @@ var _ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
var _ builder.APIGroupRouteProvider = (*APIBuilder)(nil)
|
||||
var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/ofrep")
|
||||
|
||||
const ofrepPath = "/ofrep/v1/evaluate/flags"
|
||||
|
||||
const namespaceMismatchMsg = "rejecting request with namespace mismatch"
|
||||
@@ -246,13 +243,14 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
|
||||
}
|
||||
|
||||
func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalFlag")
|
||||
ctx, span := tracing.Start(r.Context(), "ofrep.handler.evalFlag")
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
b.logger.Debug("validating namespace in oneFlagHandler handler")
|
||||
if !b.validateNamespace(r) {
|
||||
valid := b.validateNamespace(r)
|
||||
b.logger.Debug("validating namespace in oneFlagHandler handler", "valid", valid)
|
||||
if !valid {
|
||||
_ = tracing.Errorf(span, namespaceMismatchMsg)
|
||||
span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized))
|
||||
b.logger.Error(namespaceMismatchMsg)
|
||||
@@ -291,13 +289,15 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalAllFlags")
|
||||
ctx, span := tracing.Start(r.Context(), "ofrep.handler.evalAllFlags")
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
b.logger.Debug("validating namespace in allFlagsHandler handler")
|
||||
if !b.validateNamespace(r) {
|
||||
valid := b.validateNamespace(r)
|
||||
b.logger.Debug("validating namespace in allFlagsHandler handler", "valid", valid)
|
||||
|
||||
if !valid {
|
||||
_ = tracing.Errorf(span, namespaceMismatchMsg)
|
||||
span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized))
|
||||
b.logger.Error(namespaceMismatchMsg)
|
||||
@@ -327,8 +327,7 @@ func writeResponse(statusCode int, result any, logger log.Logger, w http.Respons
|
||||
|
||||
func (b *APIBuilder) namespaceFromEvalCtx(body []byte) string {
|
||||
// TODO: eval ctx should be added to span attributes, not log
|
||||
// Adding it temporary for debugging
|
||||
b.logger.Debug("evaluation context from request", "ctx", body)
|
||||
b.logger.Debug("evaluation context from request", "ctx", string(body))
|
||||
|
||||
var evalCtx struct {
|
||||
// Extract namespace from request body without consuming it
|
||||
@@ -342,9 +341,6 @@ func (b *APIBuilder) namespaceFromEvalCtx(body []byte) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Adding it temporary for debugging
|
||||
b.logger.Debug("evaluation context decoded", "namespace", evalCtx.Context.Namespace)
|
||||
|
||||
if evalCtx.Context.Namespace == "" {
|
||||
b.logger.Debug("namespace missing from evaluation context", "namespace", evalCtx.Context.Namespace)
|
||||
return ""
|
||||
@@ -364,7 +360,7 @@ func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool {
|
||||
|
||||
// validateNamespace checks if the namespace in the evaluation context matches the namespace in the request
|
||||
func (b *APIBuilder) validateNamespace(r *http.Request) bool {
|
||||
_, span := tracer.Start(r.Context(), "ofrep.validateNamespace")
|
||||
_, span := tracing.Start(r.Context(), "ofrep.validateNamespace")
|
||||
defer span.End()
|
||||
|
||||
var namespace string
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool, w http.ResponseWriter) {
|
||||
_, span := tracer.Start(ctx, "ofrep.static.evalAllFlags")
|
||||
_, span := tracing.Start(ctx, "ofrep.static.evalAllFlags")
|
||||
defer span.End()
|
||||
|
||||
result, err := b.staticEvaluator.EvalAllFlags(ctx)
|
||||
@@ -40,7 +40,7 @@ func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool,
|
||||
}
|
||||
|
||||
func (b *APIBuilder) evalFlagStatic(ctx context.Context, flagKey string, w http.ResponseWriter) {
|
||||
_, span := tracer.Start(ctx, "ofrep.static.evalFlag")
|
||||
_, span := tracing.Start(ctx, "ofrep.static.evalFlag")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("flag_key", flagKey))
|
||||
|
||||
@@ -52,6 +52,7 @@ func RegisterAPIService(
|
||||
apiregistration builder.APIRegistrar,
|
||||
) *APIBuilder {
|
||||
// Requires development settings and clearly experimental
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ func RegisterAPIService(
|
||||
extraWorkers []jobs.Worker,
|
||||
repoFactory repository.Factory,
|
||||
) (*APIBuilder, error) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import (
|
||||
//
|
||||
// The usage of strings.ToLower is because the server would convert `FromAlert` to `Fromalert`. So the make matching
|
||||
// easier, we just match all headers in lower case.
|
||||
//
|
||||
// the headers X-Real-IP and X-Forwarded-For are used by the HostedGrafanaACHeaderMiddleware at
|
||||
// https://github.com/grafana/grafana/blob/f191acf8114ab79609fc631e0f01fe2b47371188/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go#L107
|
||||
var expectedHeaders = map[string]string{
|
||||
strings.ToLower(models.FromAlertHeaderName): models.FromAlertHeaderName,
|
||||
strings.ToLower(models.CacheSkipHeaderName): models.CacheSkipHeaderName,
|
||||
@@ -37,6 +40,8 @@ var expectedHeaders = map[string]string{
|
||||
strings.ToLower(queryService.HeaderPanelPluginId): queryService.HeaderPanelPluginId,
|
||||
strings.ToLower(queryService.HeaderDashboardTitle): queryService.HeaderDashboardTitle,
|
||||
strings.ToLower(queryService.HeaderPanelTitle): queryService.HeaderPanelTitle,
|
||||
strings.ToLower("X-Real-IP"): "X-Real-IP",
|
||||
strings.ToLower("X-Forwarded-For"): "X-Forwarded-For",
|
||||
}
|
||||
|
||||
func ExtractKnownHeaders(header http.Header) map[string]string {
|
||||
|
||||
@@ -65,6 +65,7 @@ func NewQueryAPIBuilder(
|
||||
) (*QueryAPIBuilder, error) {
|
||||
// Include well typed query definitions
|
||||
var queryTypes *query.QueryTypeDefinitionList
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes) {
|
||||
// Read the expression query definitions
|
||||
raw, err := expr.QueryTypeDefinitionListJSON()
|
||||
@@ -179,6 +180,7 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// Get a list of all datasource instances
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections) {
|
||||
// Eventually this would be backed either by search or reconciler pattern
|
||||
storage[query.ConnectionResourceInfo.StoragePath()] = &connectionAccess{
|
||||
@@ -188,6 +190,7 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG
|
||||
|
||||
plugins := newPluginsStorage(b.registry)
|
||||
storage[plugins.resourceInfo.StoragePath()] = plugins
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
// The plugin registry is still experimental, and not yet accurate
|
||||
// For standard k8s api discovery to work, at least one resource must be registered
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewServiceAPIBuilder() *ServiceAPIBuilder {
|
||||
}
|
||||
|
||||
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, registerer prometheus.Registerer) *ServiceAPIBuilder {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
|
||||
return nil // skip registration unless opting into aggregator mode
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func ConvertToK8sResource(orgID int64, r definitions.Route, version string, name
|
||||
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
|
||||
Receiver: r.Receiver,
|
||||
},
|
||||
Routes: make([]model.RoutingTreeRoute, 0, len(r.Routes)),
|
||||
}
|
||||
for _, route := range r.Routes {
|
||||
if route == nil {
|
||||
|
||||
@@ -43,18 +43,22 @@ func ProvideAppInstallers(
|
||||
playlistAppInstaller,
|
||||
pluginsApplInstaller,
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
|
||||
installers = append(installers, shorturlAppInstaller)
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesAlertingRules) && rulesAppInstaller != nil {
|
||||
installers = append(installers, rulesAppInstaller)
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCorrelations) {
|
||||
installers = append(installers, correlationsAppInstaller)
|
||||
}
|
||||
if alertingNotificationAppInstaller != nil {
|
||||
installers = append(installers, alertingNotificationAppInstaller)
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesLogsDrilldown) {
|
||||
installers = append(installers, logsdrilldownAppInstaller)
|
||||
}
|
||||
@@ -97,10 +101,12 @@ func ProvideBuilderRunners(
|
||||
var apiGroupRunner *runner.APIGroupRunner
|
||||
var err error
|
||||
providers := []app.Provider{}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagInvestigationsBackend) {
|
||||
logger.Debug("Investigations backend is enabled")
|
||||
providers = append(providers, investigationAppProvider)
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) &&
|
||||
!slices.Contains(grafanaCfg.DisablePlugins, "grafana-advisor-app") {
|
||||
providers = append(providers, advisorAppProvider)
|
||||
|
||||
@@ -44,6 +44,7 @@ func RegisterAppInstaller(
|
||||
service: p,
|
||||
}
|
||||
specificConfig := any(&playlistapp.PlaylistConfig{
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
EnableReconcilers: features.IsEnabledGlobally(featuremgmt.FlagPlaylistsReconciler),
|
||||
})
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, playlistapp.New)
|
||||
|
||||
@@ -10,14 +10,13 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||
"github.com/grafana/grafana-app-sdk/simple"
|
||||
"github.com/grafana/grafana/apps/plugins/pkg/apis"
|
||||
pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis"
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,13 +37,13 @@ func RegisterAppInstaller(
|
||||
cfg: cfg,
|
||||
}
|
||||
specificConfig := any(nil)
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, pluginsapp.New)
|
||||
provider := simple.NewAppProvider(pluginsappapis.LocalManifest(), specificConfig, pluginsapp.New)
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
ManifestData: *pluginsappapis.LocalManifest().ManifestData,
|
||||
SpecificConfig: specificConfig,
|
||||
}
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{})
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, pluginsappapis.NewGoTypeAssociator())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/frontend"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -46,8 +47,9 @@ func NewModule(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend, // Ensures unified storage backend is initialized
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend)
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend, hooksService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,6 +72,7 @@ func newModuleServer(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend,
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
|
||||
@@ -93,6 +96,7 @@ func newModuleServer(opts Options,
|
||||
license: license,
|
||||
moduleRegisterer: moduleRegisterer,
|
||||
storageBackend: storageBackend,
|
||||
hooksService: hooksService,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -134,6 +138,7 @@ type ModuleServer struct {
|
||||
|
||||
// moduleRegisterer allows registration of modules provided by other builds (e.g. enterprise).
|
||||
moduleRegisterer ModuleRegisterer
|
||||
hooksService *hooks.HooksService
|
||||
}
|
||||
|
||||
// init initializes the server and its services.
|
||||
@@ -205,7 +210,7 @@ func (s *ModuleServer) Run() error {
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license)
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license, s.hooksService)
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.OperatorServer, s.initOperatorServer)
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/modules"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -330,8 +332,10 @@ func initModuleServerForTest(
|
||||
apiOpts api.ServerOptions,
|
||||
) testModuleServer {
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
hooksService := hooks.ProvideService()
|
||||
license := &licensing.OSSLicensingService{}
|
||||
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil, ProvideNoopModuleRegisterer(), nil)
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, license, ProvideNoopModuleRegisterer(), nil, hooksService)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
|
||||
|
||||
@@ -128,6 +128,7 @@ func (s *Server) Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
|
||||
if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil {
|
||||
return err
|
||||
|
||||
@@ -123,6 +123,7 @@ import (
|
||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
||||
pluginDashboards "github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/preference/prefimpl"
|
||||
promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration"
|
||||
@@ -250,6 +251,7 @@ var wireBasicSet = wire.NewSet(
|
||||
httpclientprovider.New,
|
||||
wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)),
|
||||
serverlock.ProvideService,
|
||||
wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)),
|
||||
annotationsimpl.ProvideCleanupService,
|
||||
wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)),
|
||||
cleanup.ProvideService,
|
||||
|
||||
+17
-6
File diff suppressed because one or more lines are too long
@@ -108,6 +108,7 @@ func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureTogg
|
||||
|
||||
// Run implements registry.BackgroundService
|
||||
func (r *ZanzanaReconciler) Run(ctx context.Context) error {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
||||
return r.Reconcile(ctx)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func (l *FixedRolesLoader) running(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (l *FixedRolesLoader) IsDisabled() bool {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
return !l.features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -22,6 +24,8 @@ type FolderPermissionsService struct {
|
||||
*resourcepermissions.Service
|
||||
}
|
||||
|
||||
var ErrFolderUnhandledError = errutil.Internal("folder.unhandled-error", errutil.WithPublicMessage("Unhandled folder error"))
|
||||
|
||||
var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead, accesscontrol.ActionAlertingSilencesRead}
|
||||
var FolderEditActions = append(FolderViewActions, []string{
|
||||
dashboards.ActionFoldersWrite,
|
||||
@@ -106,7 +110,16 @@ func ProvideFolderPermissions(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
switch {
|
||||
case func() bool {
|
||||
var errUtilErr errutil.Error
|
||||
return errors.As(err, &errUtilErr)
|
||||
}():
|
||||
return err
|
||||
case errors.Is(err, dashboards.ErrFolderNotFound):
|
||||
return folder.ErrFolderNotFound.Errorf("folder not found")
|
||||
}
|
||||
return ErrFolderUnhandledError.Errorf("unhandled folder error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -361,6 +361,7 @@ func (s *Service) mapPermission(permission string) ([]string, error) {
|
||||
actions = append(actions, GetActionSetName(s.options.Resource, permission))
|
||||
|
||||
// If we only want to store action sets, return now
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagOnlyStoreActionSets) {
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
@@ -406,6 +406,7 @@ func InstallAPIs(
|
||||
}
|
||||
|
||||
// if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok {
|
||||
for name := range resources {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/k8s"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// ProvideClientGenerator creates a lazy-initialized ClientGenerator.
|
||||
func ProvideClientGenerator(restConfigProvider RestConfigProvider) resource.ClientGenerator {
|
||||
return &lazyClientGenerator{
|
||||
restConfigProvider: restConfigProvider,
|
||||
}
|
||||
}
|
||||
|
||||
type lazyClientGenerator struct {
|
||||
restConfigProvider RestConfigProvider
|
||||
clientGenerator resource.ClientGenerator
|
||||
initOnce sync.Once
|
||||
initError error
|
||||
}
|
||||
|
||||
func (g *lazyClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) {
|
||||
g.initOnce.Do(func() {
|
||||
restConfig, err := g.restConfigProvider.GetRestConfig(context.Background())
|
||||
if err != nil {
|
||||
g.initError = err
|
||||
return
|
||||
}
|
||||
restConfig.APIPath = "apis"
|
||||
g.clientGenerator = k8s.NewClientRegistry(*restConfig, k8s.DefaultClientConfig())
|
||||
})
|
||||
|
||||
if g.initError != nil {
|
||||
return nil, g.initError
|
||||
}
|
||||
|
||||
return g.clientGenerator.ClientFor(kind)
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
|
||||
unifiedStorageCfg := cfg.UnifiedStorage
|
||||
o.StorageOptions.UnifiedStorageConfig = unifiedStorageCfg
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess)
|
||||
o.ExtraOptions.ExternalAddress = host
|
||||
o.ExtraOptions.APIURL = apiURL
|
||||
|
||||
@@ -423,7 +423,9 @@ func (s *service) start(ctx context.Context) error {
|
||||
delegate := server
|
||||
|
||||
var runningServer *genericapiserver.GenericAPIServer
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
isKubernetesAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
|
||||
|
||||
if isKubernetesAggregatorEnabled {
|
||||
|
||||
@@ -15,4 +15,5 @@ var WireSet = wire.NewSet(
|
||||
ProvideService,
|
||||
wire.Bind(new(Service), new(*service)),
|
||||
wire.Bind(new(builder.APIRegistrar), new(*service)),
|
||||
ProvideClientGenerator,
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC
|
||||
log := s.log.FromContext(ctx).New("tokenID", token.Id, "userID", token.UserId, "createdAt", token.CreatedAt, "rotatedAt", token.RotatedAt)
|
||||
|
||||
// Avoid multiple instances in HA mode rotating at the same time.
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) && time.Unix(token.RotatedAt, 0).Add(SkipRotationTime).After(getTime()) {
|
||||
if time.Unix(token.RotatedAt, 0).Add(SkipRotationTime).After(getTime()) {
|
||||
log.Debug("Token was last rotated very recently, skipping rotation")
|
||||
span.SetAttributes(attribute.Bool("skipped", true))
|
||||
return token, nil
|
||||
@@ -327,16 +327,13 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC
|
||||
}
|
||||
|
||||
res, err, _ := s.singleflight.Do(cmd.UnHashedToken, func() (any, error) {
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) {
|
||||
var token *auth.UserToken
|
||||
err := s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
token, err = rotate(ctx)
|
||||
return err
|
||||
})
|
||||
return token, err
|
||||
}
|
||||
return rotate(ctx)
|
||||
var token *auth.UserToken
|
||||
err := s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
token, err = rotate(ctx)
|
||||
return err
|
||||
})
|
||||
return token, err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -375,11 +372,7 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User
|
||||
|
||||
now := getTime()
|
||||
var affected int64
|
||||
withDbSession := s.sqlStore.WithDbSession
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) {
|
||||
withDbSession = s.sqlStore.WithTransactionalDbSession
|
||||
}
|
||||
err = withDbSession(ctx, func(dbSession *db.Session) error {
|
||||
err = s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
res, err := dbSession.Exec(sql, userAgent, clientIPStr, hashedToken, s.sqlStore.GetDialect().BooleanValue(false), now.Unix(), token.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -721,7 +720,6 @@ func createTestContext(t *testing.T) *testContext {
|
||||
log: log.New("test-logger"),
|
||||
singleflight: new(singleflight.Group),
|
||||
externalSessionStore: extSessionStore,
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagSkipTokenRotationIfRecent),
|
||||
tracer: tracer,
|
||||
}
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ func handleLogin(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, ident
|
||||
WriteSessionCookie(w, cfg, identity.SessionToken)
|
||||
|
||||
redirectURL := cfg.AppSubURL + "/"
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
|
||||
if redirectToCookieName != "" {
|
||||
scopedRedirectToCookie, err := r.Cookie(redirectToCookieName)
|
||||
|
||||
@@ -58,6 +58,7 @@ func ProvideRegistration(
|
||||
var passwordClients []authn.PasswordClient
|
||||
|
||||
// always register LDAP if LDAP is enabled in SSO settings
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if cfg.LDAPAuthEnabled || features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) {
|
||||
ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService, tracer)
|
||||
proxyClients = append(proxyClients, ldap)
|
||||
@@ -125,6 +126,7 @@ func ProvideRegistration(
|
||||
authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features, tracer))
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
|
||||
authnSvc.RegisterClient(clients.ProvideProvisioning())
|
||||
}
|
||||
@@ -140,11 +142,13 @@ func ProvideRegistration(
|
||||
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60)
|
||||
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagEnableSCIM) {
|
||||
authnSvc.RegisterPostAuthHook(userSync.ValidateUserProvisioningHook, 30)
|
||||
}
|
||||
|
||||
rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagCloudRBACRoles) {
|
||||
authnSvc.RegisterPostAuthHook(rbacSync.SyncCloudRoles, 110)
|
||||
authnSvc.RegisterPreLogoutHook(gcomsso.ProvideGComSSOService(cfg).LogoutHook, 50)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user