From 0b639bd1f99e0f826f1877dc01d0799f6a22aaa6 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 3 Oct 2025 14:35:03 +0200 Subject: [PATCH 01/19] Chore: Rename Azure AD to Entra ID in the authentication-related docs and on the auth UIs (#111887) * Chore: Rename Azure AD to Entra ID in the docs and on the UI * lint * i18n update --- .../configure-authentication/azuread/index.md | 54 +++++++++---------- public/app/features/auth-config/constants.ts | 2 +- public/app/features/auth-config/fields.tsx | 2 +- public/locales/en-US/grafana.json | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index f1d507bd526..6abdfb93dd4 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -12,14 +12,14 @@ labels: - cloud - enterprise - oss -menuTitle: Azure AD/Entra ID OAuth -title: Configure Azure AD/Entra ID OAuth authentication +menuTitle: Entra ID OAuth +title: Configure Entra ID OAuth authentication weight: 800 --- -# Configure Azure AD/Entra ID OAuth authentication +# Configure Entra ID OAuth authentication -The Azure AD authentication allows you to use a Microsoft Entra ID (formerly known as Azure Active Directory) tenant as an identity provider for Grafana. You can use Entra ID application roles to assign users and groups to Grafana roles from the Azure Portal. +The Entra ID authentication allows you to use a Microsoft Entra ID (formerly known as Azure Active Directory) tenant as an identity provider for Grafana. You can use Entra ID application roles to assign users and groups to Grafana roles from the Azure Portal. {{< admonition type="note" >}} If Users use the same email address in Microsoft Entra ID that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to [Using the same email address to login with different identity providers](../#using-the-same-email-address-to-login-with-different-identity-providers) for more information. @@ -27,7 +27,7 @@ If Users use the same email address in Microsoft Entra ID that they use with oth ## Create the Microsoft Entra ID application -To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. +To enable the Entra ID OAuth, register your application with Entra ID. 1. Log in to [Azure Portal](https://portal.azure.com), then click **Microsoft Entra ID** in the side menu. @@ -119,7 +119,7 @@ To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. 1. Click **Add user/group** to add a user or group to the Grafana roles. {{< admonition type="note" >}} -When assigning a group to a Grafana role, ensure that users are direct members of the group. Users in nested groups will not have access to Grafana due to limitations within Azure AD/Entra ID side. For more information, see [Microsoft Entra service limits and restrictions](https://learn.microsoft.com/en-us/entra/identity/users/directory-service-limits-restrictions). +When assigning a group to a Grafana role, ensure that users are direct members of the group. Users in nested groups will not have access to Grafana due to limitations within Entra ID side. For more information, see [Microsoft Entra service limits and restrictions](https://learn.microsoft.com/en-us/entra/identity/users/directory-service-limits-restrictions). {{< /admonition >}} ### Configure application roles for Grafana in the Azure Portal @@ -226,9 +226,9 @@ If the setting is set to `false`, the user is assigned the role of `Admin` of th Ensure that you have followed the steps in [Create the Microsoft Entra ID application](#create-the-microsoft-entra-id-application) before you begin. -## Configure Azure AD authentication client using the Grafana UI +## Configure Entra ID authentication client using the Grafana UI -As a Grafana Admin, you can configure your Azure AD/Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. +As a Grafana Admin, you can configure your Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -238,7 +238,7 @@ If you need to reset changes you made in the UI back to the default values, clic If you run Grafana in high availability mode, configuration changes may not get applied to all Grafana instances immediately. You may need to wait a few minutes for the configuration to propagate to all Grafana instances. {{< /admonition >}} -## Configure Azure AD authentication client using the Terraform provider +## Configure Entra ID authentication client using the Terraform provider ```terraform resource "grafana_sso_settings" "azuread_sso_settings" { @@ -270,17 +270,17 @@ resource "grafana_sso_settings" "azuread_sso_settings" { Refer to [Terraform Registry](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/sso_settings) for a complete reference on using the `grafana_sso_settings` resource. -## Configure Azure AD authentication client using the Grafana configuration file +## Configure Entra ID authentication client using the Grafana configuration file Ensure that you have access to the [Grafana configuration file](../../../configure-grafana/#configuration-file-location). -### Enable Azure AD OAuth in Grafana +### Enable Entra ID OAuth in Grafana Add the following to the [Grafana configuration file](../../../configure-grafana/#configuration-file-location): ``` [auth.azuread] -name = Azure AD +name = Entra ID enabled = true allow_sign_up = true auto_login = false @@ -321,7 +321,7 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired. -Refresh token fetching and access token expiration check is enabled by default for the AzureAD provider since Grafana v10.1.0. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. +Refresh token fetching and access token expiration check is enabled by default for the Entra ID provider since Grafana v10.1.0. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. {{< admonition type="note" >}} The `accessTokenExpirationCheck` feature toggle has been removed in Grafana v10.3.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. @@ -427,7 +427,7 @@ To learn more, refer to the [Team Sync](https://grafana.com/docs/grafana/::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Azure AD/Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | +| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | | `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | -| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | | `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | | `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | -| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Azure AD/Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | | +| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | | | `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | | `tls_client_cert` | No | No | The path to the certificate. | | diff --git a/public/app/features/auth-config/constants.ts b/public/app/features/auth-config/constants.ts index 954f35e7be5..3a98f08ba18 100644 --- a/public/app/features/auth-config/constants.ts +++ b/public/app/features/auth-config/constants.ts @@ -9,7 +9,7 @@ export const UIMap: Record = { google: ['google', 'Google'], generic_oauth: ['lock', 'Generic OAuth'], grafana_com: ['grafana', 'Grafana.com'], - azuread: ['microsoft', 'Azure AD'], + azuread: ['microsoft', 'Entra ID'], okta: ['okta', 'Okta'], scim: ['scim', 'SCIM'], }; diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 029e67b581a..96bb2e3ccea 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -906,7 +906,7 @@ export function fieldMap(provider: string): Record { label: t('auth-config.fields.domain-hint-label', 'Domain hint'), description: t( 'auth-config.fields.domain-hint-description', - 'Parameter to indicate the realm of the user in the Azure AD/Entra ID tenant and streamline the login process.' + 'Parameter to indicate the realm of the user in the Entra ID tenant and streamline the login process.' ), type: 'text', validation: { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 38084055225..d976f91085a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Define allowed teams IDs", "display-name-description": "Will be displayed on the login page as \"Sign in with ...\". Helpful if you use more than one identity providers or SSO protocols.", "display-name-label": "Display name", - "domain-hint-description": "Parameter to indicate the realm of the user in the Azure AD/Entra ID tenant and streamline the login process.", + "domain-hint-description": "Parameter to indicate the realm of the user in the Entra ID tenant and streamline the login process.", "domain-hint-label": "Domain hint", "domain-hint-valid-domain": "This field must be a valid domain.", "email-attribute-name-description": "Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.", From e9499bb60c044c6053e8089e5d2890b4d115426c Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:37:20 +0100 Subject: [PATCH 02/19] feat: mark Observability > Database as New! (#111938) --- pkg/services/navtree/navtreeimpl/applinks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 27a31a5bcff..0d9229a533c 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -376,7 +376,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-asserts-app": {SectionID: navtree.NavIDObservability, SortWeight: 1, Icon: "asserts"}, "grafana-kowalski-app": {SectionID: navtree.NavIDObservability, SortWeight: 2, Text: "Frontend"}, "grafana-app-observability-app": {SectionID: navtree.NavIDObservability, SortWeight: 3, Text: "Application"}, - "grafana-dbo11y-app": {SectionID: navtree.NavIDObservability, SortWeight: 4, Text: "Database"}, + "grafana-dbo11y-app": {SectionID: navtree.NavIDObservability, SortWeight: 4, Text: "Database", IsNew: true}, "grafana-k8s-app": {SectionID: navtree.NavIDObservability, SortWeight: 5, Text: "Kubernetes"}, "grafana-csp-app": {SectionID: navtree.NavIDObservability, SortWeight: 6, Icon: "cloud-provider"}, "grafana-metricsdrilldown-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"}, From 6d7c50f18d1fd0add8079ce883cdd437bfc01a94 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Fri, 3 Oct 2025 16:48:51 +0300 Subject: [PATCH 03/19] IAM: Refactor team bindings to store one pair of team and member (#111871) * store one pair of team member in team bindings object * generate queries for sql unit tests * remove TeamBinding struct --- apps/iam/kinds/v0alpha1/teambindingspec.cue | 7 ++- .../apis/iam/v0alpha1/teambinding_spec_gen.go | 28 ++++----- .../pkg/apis/iam/v0alpha1/zz_openapi_gen.go | 33 ++++------ pkg/registry/apis/iam/legacy/team.go | 34 +++-------- .../apis/iam/legacy/team_bindings_query.sql | 23 +++---- ...l--team_bindings_query-team_1_bindings.sql | 7 ++- ...am_bindings_query-team_bindings_page_1.sql | 10 +--- ...am_bindings_query-team_bindings_page_2.sql | 12 ++-- ...s--team_bindings_query-team_1_bindings.sql | 7 ++- ...am_bindings_query-team_bindings_page_1.sql | 10 +--- ...am_bindings_query-team_bindings_page_2.sql | 12 ++-- ...e--team_bindings_query-team_1_bindings.sql | 7 ++- ...am_bindings_query-team_bindings_page_1.sql | 10 +--- ...am_bindings_query-team_bindings_page_2.sql | 12 ++-- pkg/registry/apis/iam/team/store_binding.go | 34 ++++------- .../iam.grafana.app-v0alpha1.json | 60 +++++++++---------- 16 files changed, 120 insertions(+), 186 deletions(-) diff --git a/apps/iam/kinds/v0alpha1/teambindingspec.cue b/apps/iam/kinds/v0alpha1/teambindingspec.cue index 3bd130c902d..c20e592e458 100644 --- a/apps/iam/kinds/v0alpha1/teambindingspec.cue +++ b/apps/iam/kinds/v0alpha1/teambindingspec.cue @@ -4,12 +4,13 @@ TeamBindingSpec: { #Subject: { // uid of the identity name: string - // permission of the identity in the team - permission: TeamPermission } - subjects: [...#Subject] + subject: #Subject teamRef: TeamRef + + // permission of the identity in the team + permission: TeamPermission } TeamRef:{ diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go index 0e58323533e..13abd605168 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go @@ -6,8 +6,6 @@ package v0alpha1 type TeamBindingspecSubject struct { // uid of the identity Name string `json:"name"` - // permission of the identity in the team - Permission TeamBindingTeamPermission `json:"permission"` } // NewTeamBindingspecSubject creates a new TeamBindingspecSubject object. @@ -15,14 +13,6 @@ func NewTeamBindingspecSubject() *TeamBindingspecSubject { return &TeamBindingspecSubject{} } -// +k8s:openapi-gen=true -type TeamBindingTeamPermission string - -const ( - TeamBindingTeamPermissionAdmin TeamBindingTeamPermission = "admin" - TeamBindingTeamPermissionMember TeamBindingTeamPermission = "member" -) - // +k8s:openapi-gen=true type TeamBindingTeamRef struct { // Name is the unique identifier for a team. @@ -34,16 +24,26 @@ func NewTeamBindingTeamRef() *TeamBindingTeamRef { return &TeamBindingTeamRef{} } +// +k8s:openapi-gen=true +type TeamBindingTeamPermission string + +const ( + TeamBindingTeamPermissionAdmin TeamBindingTeamPermission = "admin" + TeamBindingTeamPermissionMember TeamBindingTeamPermission = "member" +) + // +k8s:openapi-gen=true type TeamBindingSpec struct { - Subjects []TeamBindingspecSubject `json:"subjects"` - TeamRef TeamBindingTeamRef `json:"teamRef"` + Subject TeamBindingspecSubject `json:"subject"` + TeamRef TeamBindingTeamRef `json:"teamRef"` + // permission of the identity in the team + Permission TeamBindingTeamPermission `json:"permission"` } // NewTeamBindingSpec creates a new TeamBindingSpec object. func NewTeamBindingSpec() *TeamBindingSpec { return &TeamBindingSpec{ - Subjects: []TeamBindingspecSubject{}, - TeamRef: *NewTeamBindingTeamRef(), + Subject: *NewTeamBindingspecSubject(), + TeamRef: *NewTeamBindingTeamRef(), } } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index cb2e31f9144..2532b5df8cc 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -2103,17 +2103,10 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "subjects": { + "subject": { SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"), - }, - }, - }, + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"), }, }, "teamRef": { @@ -2122,8 +2115,16 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef"), }, }, + "permission": { + SchemaProps: spec.SchemaProps{ + Description: "permission of the identity in the team", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, }, - Required: []string{"subjects", "teamRef"}, + Required: []string{"subject", "teamRef", "permission"}, }, }, Dependencies: []string{ @@ -2210,16 +2211,8 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref common.ReferenceCal Format: "", }, }, - "permission": { - SchemaProps: spec.SchemaProps{ - Description: "permission of the identity in the team", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"name", "permission"}, + Required: []string{"name"}, }, }, } diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index 0ad11584a7f..6f49707d5d0 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -421,7 +421,7 @@ type ListTeamBindingsQuery struct { } type ListTeamBindingsResult struct { - Bindings []TeamBinding + Bindings []TeamMember Continue int64 RV int64 } @@ -445,11 +445,6 @@ func (m TeamMember) MemberID() string { return claims.NewTypeID(claims.TypeUser, m.UserUID) } -type TeamBinding struct { - TeamUID string - Members []TeamMember -} - var sqlQueryTeamBindingsTemplate = mustTemplate("team_bindings_query.sql") type listTeamBindingsQuery struct { @@ -505,11 +500,11 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa return nil, err } - res := &ListTeamBindingsResult{} - grouped := map[string][]TeamMember{} + res := &ListTeamBindingsResult{ + Bindings: make([]TeamMember, 0, int(query.Pagination.Limit)), + } var lastID int64 - var atTeamLimit bool for rows.Next() { m := TeamMember{} @@ -518,16 +513,11 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa return res, err } - lastID = m.TeamID - members, ok := grouped[m.TeamUID] - if ok { - grouped[m.TeamUID] = append(members, m) - } else if !atTeamLimit { - grouped[m.TeamUID] = []TeamMember{m} - } + res.Bindings = append(res.Bindings, m) - if len(grouped) >= int(query.Pagination.Limit)-1 { - atTeamLimit = true + lastID = m.ID + + if len(res.Bindings) >= int(query.Pagination.Limit)-1 { res.Continue = lastID } } @@ -536,14 +526,6 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa res.RV, err = sql.GetResourceVersion(ctx, "team_member", "updated") } - res.Bindings = make([]TeamBinding, 0, len(grouped)) - for uid, members := range grouped { - res.Bindings = append(res.Bindings, TeamBinding{ - TeamUID: uid, - Members: members, - }) - } - return res, err } diff --git a/pkg/registry/apis/iam/legacy/team_bindings_query.sql b/pkg/registry/apis/iam/legacy/team_bindings_query.sql index fb8930c9fde..a64687a7e8f 100644 --- a/pkg/registry/apis/iam/legacy/team_bindings_query.sql +++ b/pkg/registry/apis/iam/legacy/team_bindings_query.sql @@ -3,18 +3,13 @@ FROM {{ .Ident .TeamMemberTable }} tm INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id WHERE -{{ if .Query.UID }} - t.uid = {{ .Arg .Query.UID }} -{{ else }} - t.uid IN( - SELECT uid - FROM {{ .Ident .TeamTable }} t - {{ if .Query.Pagination.Continue }} - WHERE t.id >= {{ .Arg .Query.Pagination.Continue }} - {{ end }} - ORDER BY t.id ASC LIMIT {{ .Arg .Query.Pagination.Limit }} - ) -{{ end }} -AND tm.org_id = {{ .Arg .Query.OrgID}} + tm.org_id = {{ .Arg .Query.OrgID}} + {{ if .Query.UID }} + AND t.uid = {{ .Arg .Query.UID }} + {{ end }} + {{- if .Query.Pagination.Continue }} + AND tm.id >= {{ .Arg .Query.Pagination.Continue }} + {{- end }} AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT {{ .Arg .Query.Pagination.Limit }}; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql index 4dc43b4f936..1b3c18c20cc 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql index be3d212eb9e..2180553e3a0 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM `grafana`.`team` t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql index ecbef2e0c01..767f1fe4c82 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM `grafana`.`team` t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql index 82d10b5fd51..50e968de4a1 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql index 77e1182cde8..76f9e5a8cca 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql index 2cec052d9da..c6608dd7968 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql index 82d10b5fd51..50e968de4a1 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql index 77e1182cde8..76f9e5a8cca 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql index 2cec052d9da..c6608dd7968 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/team/store_binding.go b/pkg/registry/apis/iam/team/store_binding.go index cdb61777039..38b2a23237f 100644 --- a/pkg/registry/apis/iam/team/store_binding.go +++ b/pkg/registry/apis/iam/team/store_binding.go @@ -117,46 +117,36 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return &list, nil } -func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0alpha1.TeamBinding { +func mapToBindingObject(ns claims.NamespaceInfo, tm legacy.TeamMember) iamv0alpha1.TeamBinding { rv := time.Time{} ct := time.Now() - for _, m := range b.Members { - if m.Updated.After(rv) { - rv = m.Updated - } - if m.Created.Before(ct) { - ct = m.Created - } + if tm.Updated.After(rv) { + rv = tm.Updated + } + if tm.Created.Before(ct) { + ct = tm.Created } return iamv0alpha1.TeamBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: b.TeamUID, + Name: tm.TeamUID, Namespace: ns.Value, ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10), CreationTimestamp: metav1.NewTime(ct), }, Spec: iamv0alpha1.TeamBindingSpec{ TeamRef: iamv0alpha1.TeamBindingTeamRef{ - Name: b.TeamUID, + Name: tm.TeamUID, }, - Subjects: mapToSubjects(b.Members), + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: tm.UserUID, + }, + Permission: common.MapTeamPermission(tm.Permission), }, } } -func mapToSubjects(members []legacy.TeamMember) []iamv0alpha1.TeamBindingspecSubject { - out := make([]iamv0alpha1.TeamBindingspecSubject, 0, len(members)) - for _, m := range members { - out = append(out, iamv0alpha1.TeamBindingspecSubject{ - Name: m.UserUID, - Permission: common.MapTeamPermission(m.Permission), - }) - } - return out -} - func mapPermisson(p team.PermissionType) iamv0.TeamPermission { if p == team.PermissionTypeAdmin { return iamv0.TeamPermissionAdmin diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index 9a7b8ef0481..3561b54c453 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -3737,20 +3737,23 @@ "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingSpec": { "type": "object", "required": [ - "subjects", - "teamRef" + "subject", + "teamRef", + "permission" ], "properties": { - "subjects": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject" - } - ] - } + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + }, + "subject": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject" + } + ] }, "teamRef": { "default": {}, @@ -3778,19 +3781,13 @@ "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject": { "type": "object", "required": [ - "name", - "permission" + "name" ], "properties": { "name": { "description": "uid of the identity", "type": "string", "default": "" - }, - "permission": { - "description": "permission of the identity in the team", - "type": "string", - "default": "" } } }, @@ -5594,15 +5591,18 @@ "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": { "type": "object", "required": [ - "subjects", - "teamRef" + "subject", + "teamRef", + "permission" ], "properties": { - "subjects": { - "type": "array", - "items": { - "default": {} - } + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + }, + "subject": { + "default": {} }, "teamRef": { "default": {} @@ -5644,19 +5644,13 @@ "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": { "type": "object", "required": [ - "name", - "permission" + "name" ], "properties": { "name": { "description": "uid of the identity", "type": "string", "default": "" - }, - "permission": { - "description": "permission of the identity in the team", - "type": "string", - "default": "" } } }, From cca9e0d55ff471f88bf909b6fece1017a9a34b6c Mon Sep 17 00:00:00 2001 From: Daniele Stefano Ferru Date: Fri, 3 Oct 2025 16:21:07 +0200 Subject: [PATCH 04/19] Provisioning: set sync state if empty (#111998) --- pkg/registry/apis/provisioning/jobs/sync/worker.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 0b0139941c0..09ed5859cc7 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -110,6 +110,10 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo lastRef := repo.Config().Status.Sync.LastRef syncStatus.LastRef = lastRef + if syncStatus.State == "" { + syncStatus.State = provisioning.JobStateWorking + } + // Update sync status at start using JSON patch patchOperations := []map[string]interface{}{ { From d0f79ee60dc3b96f310d6e9a543a2f4fe566673b Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Fri, 3 Oct 2025 10:37:49 -0400 Subject: [PATCH 05/19] Alerting: Update alerting module + refactor (#111761) * update alerting module * replace compat with ones from alerting * update type references Receiver and Integration to *Status * update route in provisioning test that is invalid after recent change * use right type for LINE ingtegration --- apps/iam/go.mod | 2 +- apps/iam/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- .../ngalert/api/api_provisioning_test.go | 11 +- .../ngalert/api/compat_contact_points.go | 11 +- .../ngalert/api/compat_contact_points_test.go | 20 ++- .../api/tooling/definitions/alertmanager.go | 6 +- pkg/services/ngalert/models/receivers.go | 9 +- pkg/services/ngalert/models/receivers_test.go | 21 ++- pkg/services/ngalert/models/testing.go | 16 +- pkg/services/ngalert/notifier/alertmanager.go | 5 +- .../alertmanager_mock/Alertmanager.go | 14 +- pkg/services/ngalert/notifier/compat.go | 36 ----- pkg/services/ngalert/notifier/compat_test.go | 143 ------------------ .../notifier/legacy_storage/receivers_test.go | 13 +- .../ngalert/notifier/receiver_svc_test.go | 3 +- .../ngalert/notifier/testreceivers.go | 9 +- .../ngalert/notifier/testreceivers_test.go | 11 +- pkg/services/ngalert/provisioning/compat.go | 8 +- .../provisioning/contactpoints_test.go | 15 +- pkg/services/ngalert/remote/alertmanager.go | 5 +- .../ngalert/remote/mock/remoteAlertmanager.go | 14 +- .../test-data/provisioning-mixed-set.yaml | 21 +-- .../notifications/receivers/receiver_test.go | 33 ++-- 25 files changed, 139 insertions(+), 297 deletions(-) delete mode 100644 pkg/services/ngalert/notifier/compat_test.go diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 9339a260a4b..9bdcbaa3bd9 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -201,7 +201,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d // indirect + github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index b9188d6bd5f..52ebbdf54dd 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -721,8 +721,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d h1:zzEty7HgfXbQ/RiBCJFMqaZiJlqiXuz/Zbc6/H6ksuM= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 h1:wfehM99Xlpltl9MQx8SITkgFgHmPGqrXoBCVLk/Q6NA= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= diff --git a/go.mod b/go.mod index 795d76c28b2..c6b9784c092 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 5b9714044a1..2f8028bb05b 100644 --- a/go.sum +++ b/go.sum @@ -1585,8 +1585,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d h1:zzEty7HgfXbQ/RiBCJFMqaZiJlqiXuz/Zbc6/H6ksuM= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 h1:wfehM99Xlpltl9MQx8SITkgFgHmPGqrXoBCVLk/Q6NA= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index a7dd8f1f2fe..db380834c4a 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -14,8 +14,7 @@ import ( "testing" "time" - alertingNotify "github.com/grafana/alerting/notify" - "github.com/grafana/alerting/receivers/schema" + "github.com/grafana/alerting/notify/notifytest" prometheus "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/timeinterval" @@ -2033,12 +2032,12 @@ func TestApiContactPointExportSnapshot(t *testing.T) { t.Run(fmt.Sprintf("exportType=%s", exportType), func(t *testing.T) { for _, redacted := range []bool{true, false} { t.Run(fmt.Sprintf("redacted=%t", redacted), func(t *testing.T) { - allIntegrations := make([]models.Integration, 0, len(alertingNotify.AllKnownConfigsForTesting)) - for integrationType := range alertingNotify.AllKnownConfigsForTesting { + allIntegrations := make([]models.Integration, 0, len(notifytest.AllKnownV1ConfigsForTesting)) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { integration := models.IntegrationGen( models.IntegrationMuts.WithName(allIntegrationsName), - models.IntegrationMuts.WithUID(fmt.Sprintf("%s-uid", integrationType)), - models.IntegrationMuts.WithValidConfig(schema.IntegrationType(integrationType)), + models.IntegrationMuts.WithUID(fmt.Sprintf("%s-uid", strings.ToLower(string(integrationType)))), + models.IntegrationMuts.WithValidConfig(integrationType), )() integration.DisableResolveMessage = redacted allIntegrations = append(allIntegrations, integration) diff --git a/pkg/services/ngalert/api/compat_contact_points.go b/pkg/services/ngalert/api/compat_contact_points.go index e18b5c0a74a..5a55bc9ca95 100644 --- a/pkg/services/ngalert/api/compat_contact_points.go +++ b/pkg/services/ngalert/api/compat_contact_points.go @@ -7,6 +7,7 @@ import ( "strings" "unsafe" + alertingModels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" "github.com/grafana/alerting/receivers" jsoniter "github.com/json-iterator/go" @@ -53,7 +54,7 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe len(cp.Threema) + len(cp.Victorops) + len(cp.Webhook) + len(cp.Wecom) + len(cp.Webex) + len(cp.Mqtt) - integration := make([]*notify.GrafanaIntegrationConfig, 0, contactPointsLength) + integration := make([]*alertingModels.IntegrationConfig, 0, contactPointsLength) var errs []error for _, i := range cp.Alertmanager { @@ -222,20 +223,20 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe return notify.APIReceiver{}, errors.Join(errs...) } contactPoint := notify.APIReceiver{ - ConfigReceiver: notify.ConfigReceiver{Name: cp.Name}, - GrafanaIntegrations: notify.GrafanaIntegrations{Integrations: integration}, + ConfigReceiver: notify.ConfigReceiver{Name: cp.Name}, + ReceiverConfig: alertingModels.ReceiverConfig{Integrations: integration}, } return contactPoint, nil } // marshallIntegration converts the API model integration to the storage model that contains settings in the JSON format. // The secret fields are not encrypted. -func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*notify.GrafanaIntegrationConfig, error) { +func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*alertingModels.IntegrationConfig, error) { data, err := json.Marshal(integration) if err != nil { return nil, fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integrationType, err) } - e := ¬ify.GrafanaIntegrationConfig{ + e := &alertingModels.IntegrationConfig{ Type: integrationType, Settings: data, } diff --git a/pkg/services/ngalert/api/compat_contact_points_test.go b/pkg/services/ngalert/api/compat_contact_points_test.go index bda8336b0bf..ef178dd085f 100644 --- a/pkg/services/ngalert/api/compat_contact_points_test.go +++ b/pkg/services/ngalert/api/compat_contact_points_test.go @@ -7,8 +7,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" + alertingmodels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" + "github.com/grafana/alerting/receivers/line" receiversTesting "github.com/grafana/alerting/receivers/testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apicompat "github.com/grafana/grafana/pkg/services/ngalert/api/compat" @@ -53,12 +57,12 @@ func TestContactPointFromContactPointExports(t *testing.T) { } // use the configs for testing because they have all fields supported by integrations - for integrationType, cfg := range notify.AllKnownConfigsForTesting { - t.Run(integrationType, func(t *testing.T) { + for integrationType, cfg := range notifytest.AllKnownV1ConfigsForTesting { + t.Run(string(integrationType), func(t *testing.T) { recCfg := ¬ify.APIReceiver{ ConfigReceiver: notify.ConfigReceiver{Name: "test-receiver"}, - GrafanaIntegrations: notify.GrafanaIntegrations{ - Integrations: []*notify.GrafanaIntegrationConfig{ + ReceiverConfig: alertingmodels.ReceiverConfig{ + Integrations: []*alertingmodels.IntegrationConfig{ cfg.GetRawNotifierConfig("test"), }, }, @@ -87,9 +91,15 @@ func TestContactPointFromContactPointExports(t *testing.T) { } if integrationType != "webhook" { // Many notifiers now support HTTPClientConfig but only Webhook currently has it enabled in schema. - //TODO: Remove this once HTTPClientConfig is added to other schemas. + // TODO: Remove this once HTTPClientConfig is added to other schemas. pathFilters = append(pathFilters, "HTTPClientConfig") } + if integrationType == line.Type { + for _, l := range actual.LineConfigs { + assert.Equal(t, "line", l.Type) + l.Type = string(line.Type) + } + } pathFilter := cmp.FilterPath(func(path cmp.Path) bool { for _, filter := range pathFilters { if strings.Contains(path.String(), filter) { diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 5c33ad658fe..530264dc287 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -603,15 +603,15 @@ type AlertGroups = amv2.AlertGroups type AlertGroup = amv2.AlertGroup -type Receiver = alertingmodels.Receiver +type Receiver = alertingmodels.ReceiverStatus // swagger:response receiversResponse type ReceiversResponse struct { // in:body - Body []alertingmodels.Receiver + Body []alertingmodels.ReceiverStatus } -type Integration = alertingmodels.Integration +type Integration = alertingmodels.IntegrationStatus // swagger:parameters RouteGetAMAlerts RouteGetAMAlertGroups RouteGetGrafanaAMAlerts RouteGetGrafanaAMAlertGroups type AlertsParams struct { diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 1f892147938..08734b0f56b 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -12,6 +12,7 @@ import ( "sort" "strings" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/alerting/receivers/schema" ) @@ -572,7 +573,7 @@ func (integration *Integration) Validate(decryptFn DecryptFn) error { return err } - return ValidateIntegration(context.Background(), alertingNotify.GrafanaIntegrationConfig{ + return ValidateIntegration(context.Background(), models.IntegrationConfig{ UID: decrypted.UID, Name: decrypted.Name, Type: decrypted.Config.Type, @@ -582,7 +583,7 @@ func (integration *Integration) Validate(decryptFn DecryptFn) error { }, alertingNotify.NoopDecrypt) } -func ValidateIntegration(ctx context.Context, integration alertingNotify.GrafanaIntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error { +func ValidateIntegration(ctx context.Context, integration models.IntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error { if integration.Type == "" { return fmt.Errorf("type should not be an empty string") } @@ -591,8 +592,8 @@ func ValidateIntegration(ctx context.Context, integration alertingNotify.Grafana } _, err := alertingNotify.BuildReceiverConfiguration(ctx, &alertingNotify.APIReceiver{ - GrafanaIntegrations: alertingNotify.GrafanaIntegrations{ - Integrations: []*alertingNotify.GrafanaIntegrationConfig{&integration}, + ReceiverConfig: models.ReceiverConfig{ + Integrations: []*models.IntegrationConfig{&integration}, }, }, alertingNotify.DecodeSecretsFromBase64, decryptFunc) if err != nil { diff --git a/pkg/services/ngalert/models/receivers_test.go b/pkg/services/ngalert/models/receivers_test.go index 0c6c08c3d93..785fffd49be 100644 --- a/pkg/services/ngalert/models/receivers_test.go +++ b/pkg/services/ngalert/models/receivers_test.go @@ -6,6 +6,7 @@ import ( "testing" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,8 +41,7 @@ func TestReceiver_EncryptDecrypt(t *testing.T) { encryptFn := Base64Enrypt decryptnFn := Base64Decrypt // Test that all known integration types encrypt and decrypt their secrets. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() encrypted := decrypedIntegration.Clone() @@ -76,8 +76,7 @@ func TestIntegration_Redact(t *testing.T) { return "TESTREDACTED" } // Test that all known integration types redact their secrets. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() @@ -106,8 +105,7 @@ func TestIntegration_Validate(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) // Test that all known integration types are valid. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() assert.NoError(t, validIntegration.Encrypt(Base64Enrypt)) @@ -242,8 +240,7 @@ func TestIntegration_WithExistingSecureFields(t *testing.T) { func TestSecretsIntegrationConfig(t *testing.T) { // Test that all known integration types have a config and correctly mark their secrets as secure. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { schemaType, ok := alertingNotify.GetSchemaForIntegration(integrationType) require.True(t, ok) @@ -272,8 +269,8 @@ func TestSecretsIntegrationConfig(t *testing.T) { } t.Run("Unknown version returns error", func(t *testing.T) { - for s := range maps.Keys(alertingNotify.AllKnownConfigsForTesting) { - schemaType, _ := alertingNotify.GetSchemaForIntegration(schema.IntegrationType(s)) + for s := range maps.Keys(notifytest.AllKnownV1ConfigsForTesting) { + schemaType, _ := alertingNotify.GetSchemaForIntegration(s) _, err := IntegrationConfigFromSchema(schemaType, "unknown") require.Error(t, err) return @@ -285,8 +282,8 @@ func TestIntegration_SecureFields(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) // Test that all known integration types have a config and correctly mark their secrets as secure. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for it := range notifytest.AllKnownV1ConfigsForTesting { + integrationType := it t.Run(string(integrationType), func(t *testing.T) { t.Run("contains SecureSettings", func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 2164ca8f86f..0fd681b693c 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -13,6 +13,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/google/uuid" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/grafana/alerting/receivers/webex" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -1270,7 +1271,7 @@ func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integr func IntegrationGen(mutators ...Mutator[Integration]) func() Integration { return func() Integration { name := util.GenerateShortUID() - randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting) + randomIntegrationType, _ := randomMapKey(notifytest.AllKnownV1ConfigsForTesting) c := Integration{ UID: util.GenerateShortUID(), @@ -1280,7 +1281,7 @@ func IntegrationGen(mutators ...Mutator[Integration]) func() Integration { SecureSettings: make(map[string]string), } - IntegrationMuts.WithValidConfig(schema.IntegrationType(randomIntegrationType))(&c) + IntegrationMuts.WithValidConfig(randomIntegrationType)(&c) for _, mutator := range mutators { mutator(&c) @@ -1317,7 +1318,11 @@ func (n IntegrationMutators) WithName(name string) Mutator[Integration] { func (n IntegrationMutators) WithValidConfig(integrationType schema.IntegrationType) Mutator[Integration] { return func(c *Integration) { // TODO add support for v0 integrations - config := alertingNotify.AllKnownConfigsForTesting[string(integrationType)].GetRawNotifierConfig(c.Name) + ncfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType] + if !ok { + panic(fmt.Sprintf("unknown integration type: %s", integrationType)) + } + config := ncfg.GetRawNotifierConfig(c.Name) typeSchema, _ := alertingNotify.GetSchemaForIntegration(integrationType) integrationConfig, _ := IntegrationConfigFromSchema(typeSchema, schema.V1) c.Config = integrationConfig @@ -1337,7 +1342,10 @@ func (n IntegrationMutators) WithValidConfig(integrationType schema.IntegrationT func (n IntegrationMutators) WithInvalidConfig(integrationType schema.IntegrationType) Mutator[Integration] { return func(c *Integration) { - typeSchema, _ := alertingNotify.GetSchemaForIntegration(integrationType) + typeSchema, ok := alertingNotify.GetSchemaForIntegration(integrationType) + if !ok { + panic(fmt.Sprintf("unknown integration type: %s", integrationType)) + } c.Config, _ = IntegrationConfigFromSchema(typeSchema, schema.V1) c.Settings = map[string]interface{}{} c.SecureSettings = map[string]string{} diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 06c5b5e0cce..f4454d87c5f 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/alerting/notify/nfstatus" "github.com/prometheus/alertmanager/config" @@ -365,7 +366,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable return false, nil } - receivers := PostableApiAlertingConfigToApiReceivers(amConfig) + receivers := alertingNotify.PostableAPIReceiversToAPIReceivers(amConfig.Receivers) for _, recv := range receivers { err = patchNewSecureFields(ctx, recv, alertingNotify.DecodeSecretsFromBase64, am.decryptFn) if err != nil { @@ -406,7 +407,7 @@ func patchNewSecureFields(ctx context.Context, api *alertingNotify.APIReceiver, return nil } -func patchSettingsFromSecureSettings(ctx context.Context, integration *alertingNotify.GrafanaIntegrationConfig, key string, decode alertingNotify.DecodeSecretsFn, decrypt alertingNotify.GetDecryptedValueFn) error { +func patchSettingsFromSecureSettings(ctx context.Context, integration *models.IntegrationConfig, key string, decode alertingNotify.DecodeSecretsFn, decrypt alertingNotify.GetDecryptedValueFn) error { if _, ok := integration.SecureSettings[key]; !ok { return nil } diff --git a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go index 20f4e14d0ab..e4ec918e59d 100644 --- a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go @@ -309,23 +309,23 @@ func (_c *AlertmanagerMock_GetAlerts_Call) RunAndReturn(run func(context.Context } // GetReceivers provides a mock function with given fields: ctx -func (_m *AlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.Receiver, error) { +func (_m *AlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.ReceiverStatus, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetReceivers") } - var r0 []alertingmodels.Receiver + var r0 []alertingmodels.ReceiverStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.Receiver, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.ReceiverStatus, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.Receiver); ok { + if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.ReceiverStatus); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]alertingmodels.Receiver) + r0 = ret.Get(0).([]alertingmodels.ReceiverStatus) } } @@ -356,12 +356,12 @@ func (_c *AlertmanagerMock_GetReceivers_Call) Run(run func(ctx context.Context)) return _c } -func (_c *AlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.Receiver, _a1 error) *AlertmanagerMock_GetReceivers_Call { +func (_c *AlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.ReceiverStatus, _a1 error) *AlertmanagerMock_GetReceivers_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *AlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.Receiver, error)) *AlertmanagerMock_GetReceivers_Call { +func (_c *AlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.ReceiverStatus, error)) *AlertmanagerMock_GetReceivers_Call { _c.Call.Return(run) return _c } diff --git a/pkg/services/ngalert/notifier/compat.go b/pkg/services/ngalert/notifier/compat.go index 9b2dccb8099..63c6ed7d7bb 100644 --- a/pkg/services/ngalert/notifier/compat.go +++ b/pkg/services/ngalert/notifier/compat.go @@ -1,47 +1,11 @@ package notifier import ( - "encoding/json" - alertingNotify "github.com/grafana/alerting/notify" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func PostableGrafanaReceiverToGrafanaIntegrationConfig(p *apimodels.PostableGrafanaReceiver) *alertingNotify.GrafanaIntegrationConfig { - return &alertingNotify.GrafanaIntegrationConfig{ - UID: p.UID, - Name: p.Name, - Type: p.Type, - DisableResolveMessage: p.DisableResolveMessage, - Settings: json.RawMessage(p.Settings), - SecureSettings: p.SecureSettings, - } -} - -func PostableApiReceiverToApiReceiver(r *apimodels.PostableApiReceiver) *alertingNotify.APIReceiver { - integrations := alertingNotify.GrafanaIntegrations{ - Integrations: make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)), - } - for _, cfg := range r.GrafanaManagedReceivers { - integrations.Integrations = append(integrations.Integrations, PostableGrafanaReceiverToGrafanaIntegrationConfig(cfg)) - } - - return &alertingNotify.APIReceiver{ - ConfigReceiver: r.Receiver, - GrafanaIntegrations: integrations, - } -} - -func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConfig) []*alertingNotify.APIReceiver { - apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) - for _, receiver := range c.Receivers { - apiReceivers = append(apiReceivers, PostableApiReceiverToApiReceiver(receiver)) - } - return apiReceivers -} - // Silence-specific compat functions to convert between grafana/alerting and model types. func GettableSilenceToSilence(s alertingNotify.GettableSilence) *models.Silence { diff --git a/pkg/services/ngalert/notifier/compat_test.go b/pkg/services/ngalert/notifier/compat_test.go deleted file mode 100644 index b3a2ff8508a..00000000000 --- a/pkg/services/ngalert/notifier/compat_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package notifier - -import ( - "encoding/json" - "testing" - - alertingNotify "github.com/grafana/alerting/notify" - "github.com/prometheus/alertmanager/config" - "github.com/stretchr/testify/require" - - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" -) - -func TestPostableGrafanaReceiverToGrafanaIntegrationConfig(t *testing.T) { - r := &apimodels.PostableGrafanaReceiver{ - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - } - actual := PostableGrafanaReceiverToGrafanaIntegrationConfig(r) - require.Equal(t, alertingNotify.GrafanaIntegrationConfig{ - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: json.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, *actual) -} - -func TestPostableApiReceiverToApiReceiver(t *testing.T) { - t.Run("returns empty when no receivers", func(t *testing.T) { - r := &apimodels.PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "test-receiver", - }, - } - actual := PostableApiReceiverToApiReceiver(r) - require.Empty(t, actual.Integrations) - require.Equal(t, r.Receiver, actual.ConfigReceiver) - }) - t.Run("converts receivers", func(t *testing.T) { - r := &apimodels.PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "test-receiver", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - { - UID: "test-uid2", - Name: "test-name2", - Type: "webhook", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data2" : "test2" }`), - SecureSettings: map[string]string{ - "test2": "data2", - }, - }, - }, - }, - } - actual := PostableApiReceiverToApiReceiver(r) - require.Len(t, actual.Integrations, 2) - require.Equal(t, r.Receiver, actual.ConfigReceiver) - require.Equal(t, *PostableGrafanaReceiverToGrafanaIntegrationConfig(r.GrafanaManagedReceivers[0]), *actual.Integrations[0]) - require.Equal(t, *PostableGrafanaReceiverToGrafanaIntegrationConfig(r.GrafanaManagedReceivers[1]), *actual.Integrations[1]) - }) -} - -func TestPostableApiAlertingConfigToApiReceivers(t *testing.T) { - t.Run("returns empty when no receivers", func(t *testing.T) { - r := apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{}, - } - actual := PostableApiAlertingConfigToApiReceivers(r) - require.Empty(t, actual) - }) - c := apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{}, - Receivers: []*apimodels.PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "test-receiver", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - }, - }, - }, - { - Receiver: config.Receiver{ - Name: "test-receiver2", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid2", - Name: "test-name1", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - }, - }, - }, - }, - } - actual := PostableApiAlertingConfigToApiReceivers(c) - - require.Len(t, actual, 2) - require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[0]), actual[0]) - require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[1]), actual[1]) -} diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go index 72ce37763aa..a020b833de8 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/alerting/definition" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/grafana/alerting/receivers/webhook" "github.com/prometheus/alertmanager/config" @@ -92,7 +93,7 @@ func TestDeleteReceiver(t *testing.T) { } func TestCreateReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -199,7 +200,7 @@ func TestCreateReceiver(t *testing.T) { } func TestUpdateReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -300,7 +301,7 @@ func TestUpdateReceiver(t *testing.T) { } func TestGetReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -491,7 +492,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-1", Type: "webhook", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["webhook"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["webhook"].Config), }, }, }, @@ -503,7 +504,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-2", Type: "webhook", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["webhook"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["webhook"].Config), }, }, }, @@ -515,7 +516,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-3", Type: "email", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["email"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["email"].Config), }, }, }, diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 5cf9146bb1c..8816124334d 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/grafana/alerting/receivers/line" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -352,7 +353,7 @@ func TestReceiverService_Create(t *testing.T) { slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))() emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))() - lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("line"))() + lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig(line.Type))() baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))() for _, tc := range []struct { diff --git a/pkg/services/ngalert/notifier/testreceivers.go b/pkg/services/ngalert/notifier/testreceivers.go index 187caab480b..41bd2a181c7 100644 --- a/pkg/services/ngalert/notifier/testreceivers.go +++ b/pkg/services/ngalert/notifier/testreceivers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" v2 "github.com/prometheus/alertmanager/api/v2" @@ -13,9 +14,9 @@ import ( func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) { receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) for _, r := range c.Receivers { - integrations := make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)) + integrations := make([]*models.IntegrationConfig, 0, len(r.GrafanaManagedReceivers)) for _, gr := range r.GrafanaManagedReceivers { - integrations = append(integrations, &alertingNotify.GrafanaIntegrationConfig{ + integrations = append(integrations, &models.IntegrationConfig{ UID: gr.UID, Name: gr.Name, Type: gr.Type, @@ -26,7 +27,7 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } recv := &alertingNotify.APIReceiver{ ConfigReceiver: r.Receiver, - GrafanaIntegrations: alertingNotify.GrafanaIntegrations{ + ReceiverConfig: models.ReceiverConfig{ Integrations: integrations, }, } @@ -52,5 +53,5 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } func (am *alertmanager) GetReceivers(_ context.Context) ([]apimodels.Receiver, error) { - return am.Base.GetReceivers(), nil + return am.Base.GetReceiversStatus(), nil } diff --git a/pkg/services/ngalert/notifier/testreceivers_test.go b/pkg/services/ngalert/notifier/testreceivers_test.go index 6ee01b9b9fe..b367a8500ea 100644 --- a/pkg/services/ngalert/notifier/testreceivers_test.go +++ b/pkg/services/ngalert/notifier/testreceivers_test.go @@ -6,13 +6,14 @@ import ( "net/url" "testing" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/stretchr/testify/require" ) func TestInvalidReceiverError_Error(t *testing.T) { e := alertingNotify.IntegrationValidationError{ - Integration: &alertingNotify.GrafanaIntegrationConfig{ + Integration: &models.IntegrationConfig{ Name: "test", Type: "test-type", UID: "uid", @@ -24,7 +25,7 @@ func TestInvalidReceiverError_Error(t *testing.T) { func TestReceiverTimeoutError_Error(t *testing.T) { e := alertingNotify.IntegrationTimeoutError{ - Integration: &alertingNotify.GrafanaIntegrationConfig{ + Integration: &models.IntegrationConfig{ Name: "test", UID: "uid", }, @@ -45,7 +46,7 @@ func (e timeoutError) Timeout() bool { func TestProcessNotifierError(t *testing.T) { t.Run("assert ReceiverTimeoutError is returned for context deadline exceeded", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } @@ -56,7 +57,7 @@ func TestProcessNotifierError(t *testing.T) { }) t.Run("assert ReceiverTimeoutError is returned for *url.Error timeout", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } @@ -72,7 +73,7 @@ func TestProcessNotifierError(t *testing.T) { }) t.Run("assert unknown error is returned unmodified", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } diff --git a/pkg/services/ngalert/provisioning/compat.go b/pkg/services/ngalert/provisioning/compat.go index 13d0bba88a9..a4b3ea50922 100644 --- a/pkg/services/ngalert/provisioning/compat.go +++ b/pkg/services/ngalert/provisioning/compat.go @@ -3,19 +3,19 @@ package provisioning import ( "strings" - alertingNotify "github.com/grafana/alerting/notify" + alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func EmbeddedContactPointToGrafanaIntegrationConfig(e definitions.EmbeddedContactPoint) (alertingNotify.GrafanaIntegrationConfig, error) { +func EmbeddedContactPointToGrafanaIntegrationConfig(e definitions.EmbeddedContactPoint) (alertingModels.IntegrationConfig, error) { data, err := e.Settings.MarshalJSON() if err != nil { - return alertingNotify.GrafanaIntegrationConfig{}, err + return alertingModels.IntegrationConfig{}, err } - return alertingNotify.GrafanaIntegrationConfig{ + return alertingModels.IntegrationConfig{ UID: e.UID, Name: e.Name, Type: e.Type, diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index b85dfcd3108..1a281f912b6 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" @@ -425,7 +426,7 @@ func TestIntegrationContactPointServiceDecryptRedact(t *testing.T) { } func TestRemoveSecretsForContactPoint(t *testing.T) { - overrides := map[string]func(settings map[string]any){ + overrides := map[schema.IntegrationType]func(settings map[string]any){ "webhook": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password settings["authorization_credentials"] = "test-authz-creds" }, @@ -437,23 +438,23 @@ func TestRemoveSecretsForContactPoint(t *testing.T) { }, } - configs := notify.AllKnownConfigsForTesting + configs := notifytest.AllKnownV1ConfigsForTesting keys := maps.Keys(configs) slices.Sort(keys) for _, integrationType := range keys { - integration := models.IntegrationGen(models.IntegrationMuts.WithValidConfig(schema.IntegrationType(integrationType)))() + integration := models.IntegrationGen(models.IntegrationMuts.WithValidConfig(integrationType))() if f, ok := overrides[integrationType]; ok { f(integration.Settings) } settingsRaw, err := json.Marshal(integration.Settings) require.NoError(t, err) - typeSchema, _ := notify.GetSchemaVersionForIntegration(schema.IntegrationType(integrationType), schema.V1) + typeSchema, _ := notify.GetSchemaVersionForIntegration(integrationType, schema.V1) expectedFields := typeSchema.GetSecretFieldsPaths() - t.Run(integrationType, func(t *testing.T) { + t.Run(string(integrationType), func(t *testing.T) { cp := definitions.EmbeddedContactPoint{ - Name: "integration-" + integrationType, - Type: integrationType, + Name: "integration-" + string(integrationType), + Type: string(integrationType), Settings: simplejson.MustJson(settingsRaw), } secureFields, err := RemoveSecretsForContactPoint(&cp) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 7048810195f..8cde9ee198f 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -609,10 +609,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei return nil, 0, fmt.Errorf("failed to decrypt receivers: %w", err) } - apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) - for _, r := range decryptedReceivers { - apiReceivers = append(apiReceivers, notifier.PostableApiReceiverToApiReceiver(r)) - } + apiReceivers := alertingNotify.PostableAPIReceiversToAPIReceivers(decryptedReceivers) var alert *alertingNotify.TestReceiversConfigAlertParams if c.Alert != nil { alert = &alertingNotify.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} diff --git a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go index 1199b65ae9c..8f8e506c765 100644 --- a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go +++ b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go @@ -358,23 +358,23 @@ func (_c *RemoteAlertmanagerMock_GetAlerts_Call) RunAndReturn(run func(context.C } // GetReceivers provides a mock function with given fields: ctx -func (_m *RemoteAlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.Receiver, error) { +func (_m *RemoteAlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.ReceiverStatus, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetReceivers") } - var r0 []alertingmodels.Receiver + var r0 []alertingmodels.ReceiverStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.Receiver, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.ReceiverStatus, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.Receiver); ok { + if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.ReceiverStatus); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]alertingmodels.Receiver) + r0 = ret.Get(0).([]alertingmodels.ReceiverStatus) } } @@ -405,12 +405,12 @@ func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Run(run func(ctx context.Con return _c } -func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.Receiver, _a1 error) *RemoteAlertmanagerMock_GetReceivers_Call { +func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.ReceiverStatus, _a1 error) *RemoteAlertmanagerMock_GetReceivers_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *RemoteAlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.Receiver, error)) *RemoteAlertmanagerMock_GetReceivers_Call { +func (_c *RemoteAlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.ReceiverStatus, error)) *RemoteAlertmanagerMock_GetReceivers_Call { _c.Call.Return(run) return _c } diff --git a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml index b8845d29548..41b63fc2ed6 100644 --- a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml +++ b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml @@ -144,16 +144,17 @@ policies: - label_keys_not_$$escaped # a list of prometheus-like matchers that an alert rule has to fulfill to match the node (allowed chars # [a-zA-Z_:]) - matchers: - - alertname = Watchdog - - service_id_X = serviceX - - severity =~ "warning|critical" - # a list of grafana-like matchers that an alert rule has to fulfill to match the node - object_matchers: - - ["alertname", "=", "CPUUsage"] - - ["service_id-X", "=", "serviceX"] - - ["severity", "=~", "warning|critical"] group_wait: 30s group_interval: 5m repeat_interval: 4h - routes: [] + routes: + - matchers: + - alertname = Watchdog + - service_id_X = serviceX + - severity =~ "warning|critical" + # a list of grafana-like matchers that an alert rule has to fulfill to match the node + object_matchers: + - [ "alertname", "=", "CPUUsage" ] + - [ "service_id-X", "=", "serviceX" ] + - [ "severity", "=~", "warning|critical" ] + diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 1d34b5106c8..da834b690e8 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -9,10 +9,11 @@ import ( "net/http" "path" "slices" - "sort" "strings" "testing" + "github.com/grafana/alerting/notify/notifytest" + "github.com/grafana/alerting/receivers/line" "github.com/grafana/alerting/receivers/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1225,7 +1226,7 @@ func TestIntegrationCRUD(t *testing.T) { t.Run("should be able to update default receiver", func(t *testing.T) { require.NotNil(t, defaultReceiver) newDefault := defaultReceiver.Copy().(*v0alpha1.Receiver) - newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, "line")) + newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, line.Type)) updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{}) require.NoError(t, err) @@ -1266,10 +1267,10 @@ func TestIntegrationCRUD(t *testing.T) { var receiver *v0alpha1.Receiver t.Run("should correctly persist all known integrations", func(t *testing.T) { - integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notify.AllKnownConfigsForTesting)) - keysIter := maps.Keys(notify.AllKnownConfigsForTesting) + integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notifytest.AllKnownV1ConfigsForTesting)) + keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting) keys := slices.Collect(keysIter) - sort.Strings(keys) + slices.Sort(keys) for _, key := range keys { integrations = append(integrations, createIntegration(t, key)) } @@ -1300,7 +1301,7 @@ func TestIntegrationCRUD(t *testing.T) { export := legacyCli.ExportReceiverTyped(t, receiver.Spec.Title, true) for _, integration := range export.Receivers { - expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] // to lower because there is LINE that is in different casing in API + expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)] assert.JSONEqf(t, expected.Config, string(integration.Settings), "integration %s", integration.Type) } }) @@ -1313,7 +1314,7 @@ func TestIntegrationCRUD(t *testing.T) { for _, integration := range get.Spec.Integrations { integrationType := schema.IntegrationType(integration.Type) t.Run(integration.Type, func(t *testing.T) { - expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] + expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)] var fields map[string]any require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields)) typeSchema, ok := notify.GetSchemaVersionForIntegration(integrationType, schema.V1) @@ -1336,11 +1337,11 @@ func TestIntegrationCRUD(t *testing.T) { }) t.Run("should fail to persist receiver with invalid config", func(t *testing.T) { - keysIter := maps.Keys(notify.AllKnownConfigsForTesting) + keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting) keys := slices.Collect(keysIter) - sort.Strings(keys) + slices.Sort(keys) for _, key := range keys { - t.Run(key, func(t *testing.T) { + t.Run(string(key), func(t *testing.T) { integration := createIntegration(t, key) // Make the integration invalid, so it fails to create. This is usually done by sending empty settings. clear(integration.Settings) @@ -1503,18 +1504,18 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) require.NoError(t, err) } -func createIntegration(t *testing.T, integrationType string) v0alpha1.ReceiverIntegration { - cfg, ok := notify.AllKnownConfigsForTesting[integrationType] +func createIntegration(t *testing.T, integrationType schema.IntegrationType) v0alpha1.ReceiverIntegration { + cfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType] require.Truef(t, ok, "no known config for integration type %s", integrationType) - return createIntegrationWithSettings(t, integrationType, "v1", cfg.Config) + return createIntegrationWithSettings(t, integrationType, schema.V1, cfg.Config) } -func createIntegrationWithSettings(t *testing.T, integrationType string, integrationVersion string, settingsJson string) v0alpha1.ReceiverIntegration { +func createIntegrationWithSettings(t *testing.T, integrationType schema.IntegrationType, integrationVersion schema.Version, settingsJson string) v0alpha1.ReceiverIntegration { settings := common.Unstructured{} require.NoError(t, settings.UnmarshalJSON([]byte(settingsJson))) return v0alpha1.ReceiverIntegration{ Settings: settings.Object, - Type: integrationType, - Version: integrationVersion, + Type: string(integrationType), + Version: string(integrationVersion), DisableResolveMessage: util.Pointer(false), } } From 43be84076cc3898d31ff23a2c07b5217637946a3 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 3 Oct 2025 16:43:03 +0200 Subject: [PATCH 06/19] Alerting: Migrate `spec.title` and `spec.name` fieldSelectors (#111993) Migrate `spec.title` and `spec.name` fieldSelectors to use base64URL encoded `metadata.name` selectors since the `spec` properties aren't indexed and no longer searchable in the new app platform API. --- eslint-suppressions.json | 5 -- .../src/grafana/api/util.test.ts | 70 ++++++++++++++++++- .../grafana-alerting/src/grafana/api/util.ts | 36 ++++++++++ packages/grafana-alerting/src/index.ts | 3 + .../mute-timings/MuteTimingsTable.test.tsx | 6 +- .../mute-timings/useMuteTimings.tsx | 9 ++- .../contactPoint/ContactPointSelector.tsx | 13 +++- .../notificaton-preview/ContactPointGroup.tsx | 5 +- .../rule-viewer/ContactPointLink.tsx | 8 ++- .../server/handlers/k8s/timeIntervals.k8s.ts | 15 ++-- .../alerting/unified/mocks/server/utils.ts | 3 +- .../alerting/unified/utils/k8s/utils.ts | 4 +- 12 files changed, 151 insertions(+), 26 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 15dbecf1ed4..43845407d57 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1638,11 +1638,6 @@ "count": 8 } }, - "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/ActiveTimingFields.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/packages/grafana-alerting/src/grafana/api/util.test.ts b/packages/grafana-alerting/src/grafana/api/util.test.ts index 215cba04187..f8278bbdc72 100644 --- a/packages/grafana-alerting/src/grafana/api/util.test.ts +++ b/packages/grafana-alerting/src/grafana/api/util.test.ts @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; -import { getAPIBaseURL, getAPINamespace, getAPIReducerPath } from './util'; +import { base64UrlEncode, getAPIBaseURL, getAPINamespace, getAPIReducerPath } from './util'; describe('API utilities', () => { const originalAppSubUrl = config.appSubUrl; @@ -64,4 +64,72 @@ describe('API utilities', () => { expect(result).toBe('notifications.alerting.grafana.app/v0alpha1'); }); }); + + describe('base64UrlEncode', () => { + it('should encode simple ASCII strings', () => { + expect(base64UrlEncode('hello')).toBe('aGVsbG8'); + }); + + it('should encode strings with special characters', () => { + expect(base64UrlEncode('hello world!')).toBe('aGVsbG8gd29ybGQh'); + }); + + it('should handle emoji characters correctly', () => { + // Single emoji + expect(base64UrlEncode('⛳')).toBe('4puz'); + // Multi-byte emoji + expect(base64UrlEncode('🧀')).toBe('8J-ngA'); + // Emoji with variant selector + expect(base64UrlEncode('❤️')).toBe('4p2k77iP'); + }); + + it('should handle mixed ASCII and Unicode characters', () => { + const input = 'hello⛳❤️🧀'; + const encoded = base64UrlEncode(input); + expect(encoded).toBe('aGVsbG_im7PinaTvuI_wn6eA'); + }); + + it('should convert to base64url format (no padding)', () => { + // Standard base64 would have padding with '=' + const result = base64UrlEncode('test'); + expect(result).not.toContain('='); + }); + + it('should replace + with - and / with _', () => { + // String that produces both + and / in standard base64 + const input = 'a??b'; // produces 'YT8/Yg==' in base64, which has / + const input2 = 'a?>b'; // produces 'YT8+Yg==' in base64, which has + + const encoded = base64UrlEncode(input); + const encoded2 = base64UrlEncode(input2); + expect(encoded).not.toContain('+'); + expect(encoded).not.toContain('/'); + expect(encoded2).not.toContain('+'); + expect(encoded2).not.toContain('/'); + expect(encoded).toContain('_'); // Should have _ instead of / + expect(encoded2).toContain('-'); // Should have - instead of + + }); + + it('should handle empty strings', () => { + expect(base64UrlEncode('')).toBe(''); + }); + + it('should handle contact point names with special characters', () => { + expect(base64UrlEncode('my-contact-point')).toBe('bXktY29udGFjdC1wb2ludA'); + expect(base64UrlEncode('Contact Point 🔔')).toBe('Q29udGFjdCBQb2ludCDwn5SU'); + }); + + it('should throw error for malformed UTF-16 strings with lone surrogates', () => { + // String with lone high surrogate + const malformedString = 'hello\uDE75'; + expect(() => base64UrlEncode(malformedString)).toThrow( + 'Cannot encode malformed UTF-16 string with lone surrogates' + ); + }); + + it('should handle well-formed strings with proper surrogate pairs', () => { + // Proper surrogate pair for emoji (U+1F9C0) + const wellFormedString = 'hello\uD83E\uDDC0'; + expect(() => base64UrlEncode(wellFormedString)).not.toThrow(); + }); + }); }); diff --git a/packages/grafana-alerting/src/grafana/api/util.ts b/packages/grafana-alerting/src/grafana/api/util.ts index a562e151366..cc7787716a4 100644 --- a/packages/grafana-alerting/src/grafana/api/util.ts +++ b/packages/grafana-alerting/src/grafana/api/util.ts @@ -13,3 +13,39 @@ export const getAPIBaseURL = (group: string, version: string) => { // By including the version in the reducer path we can prevent cache bugs when different versions of the API are used for the same entities export const getAPIReducerPath = (group: string, version: string) => `${group}/${version}` as const; + +/** + * Check if a string is well-formed UTF-16 (no lone surrogates). + * encodeURIComponent() throws an error for lone surrogates + */ +export const isWellFormed = (str: string): boolean => { + try { + encodeURIComponent(str); + return true; + } catch (error) { + return false; + } +}; + +/** + * Base64URL encode a string using native browser APIs. + * Handles Unicode characters correctly by using TextEncoder. + * Converts standard base64 to base64url by replacing + with -, / with _, and removing padding. + * @throws Error if the input string contains lone surrogates (malformed UTF-16) + */ +export const base64UrlEncode = (value: string): string => { + // Check if the string is well-formed UTF-16 + if (!isWellFormed(value)) { + throw new Error(`Cannot encode malformed UTF-16 string with lone surrogates: ${value}`); + } + + // Encode UTF-8 string to bytes + const bytes = new TextEncoder().encode(value); + + // Convert bytes to base64 + const binString = String.fromCodePoint(...bytes); + const base64 = btoa(binString); + + // Convert to base64url format + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; diff --git a/packages/grafana-alerting/src/index.ts b/packages/grafana-alerting/src/index.ts index 288d2c776fd..e7e8f4b22d2 100644 --- a/packages/grafana-alerting/src/index.ts +++ b/packages/grafana-alerting/src/index.ts @@ -11,5 +11,8 @@ export { AlertLabels } from './grafana/rules/components/labels/AlertLabels'; export { AlertLabel } from './grafana/rules/components/labels/AlertLabel'; // keep label utils internal to the app for now +// Utilities +export { base64UrlEncode } from './grafana/api/util'; + // This is a dummy export so typescript doesn't error importing an "empty module" export const index = {}; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index d9d611d7a00..0bce93870f9 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -1,5 +1,6 @@ import { render, screen, userEvent, within } from 'test/test-utils'; +import { base64UrlEncode } from '@grafana/alerting'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setMuteTimingsListError, @@ -10,7 +11,7 @@ import { captureRequests } from 'app/features/alerting/unified/mocks/server/even import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../../mocks'; -import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; +import { TIME_INTERVAL_NAME_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -113,8 +114,9 @@ describe('MuteTimingsTable', () => { await user.click(await screen.findByRole('button', { name: /delete/i })); const requests = await capture; + const encodedName = base64UrlEncode(TIME_INTERVAL_NAME_HAPPY_PATH); const deleteRequest = requests.find( - (r) => r.url.includes(`timeintervals/${TIME_INTERVAL_UID_HAPPY_PATH}`) && r.method === 'DELETE' + (r) => r.url.includes(`timeintervals/${encodedName}`) && r.method === 'DELETE' ); expect(deleteRequest).toBeDefined(); diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index aad08e47548..94e290a2087 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; import { timeIntervalsApi } from 'app/features/alerting/unified/api/timeIntervalsApi'; import { mergeTimeIntervals } from 'app/features/alerting/unified/components/mute-timings/util'; @@ -10,9 +11,9 @@ import { import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { - encodeFieldSelector, isK8sEntityProvisioned, shouldUseK8sApi, + stringifyFieldSelector, } from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; @@ -203,8 +204,10 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma useEffect(() => { if (useK8sApi) { const namespace = getAPINamespace(); - const entityName = encodeFieldSelector(nameToFind); - getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${entityName}` }, true); + getGrafanaTimeInterval( + { namespace, fieldSelector: stringifyFieldSelector([['metadata.name', base64UrlEncode(nameToFind)]]) }, + true + ); } else { getAlertmanagerTimeInterval(alertmanager, true); } diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index c512615ce78..a71930c8b88 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -3,10 +3,12 @@ import { isEmpty } from 'lodash'; import { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { base64UrlEncode } from '@grafana/alerting'; import { ContactPointSelector as GrafanaManagedContactPointSelector, alertingAPI } from '@grafana/alerting/unstable'; import { Trans, t } from '@grafana/i18n'; import { Field, FieldValidationMessage, Stack, TextLink } from '@grafana/ui'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { stringifyFieldSelector } from 'app/features/alerting/unified/utils/k8s/utils'; import { createRelativeUrl } from 'app/features/alerting/unified/utils/url'; export interface ContactPointSelectorProps { @@ -21,9 +23,13 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps // check if the contact point still exists, we'll use listReceiver to check if the contact point exists because getReceiver doesn't work with // contact point titles but with UUIDs (which is not what we store on the alert rule definition) - const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: `spec.title=${contactPointInForm}`, - }); + const encodedContactPoint = contactPointInForm ? base64UrlEncode(contactPointInForm) : ''; + const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery( + { + fieldSelector: stringifyFieldSelector([['metadata.name', encodedContactPoint]]), + }, + { skip: !contactPointInForm } + ); const contactPointNotFound = contactPointInForm && status === QueryStatus.fulfilled && isEmpty(currentData?.items); @@ -37,6 +43,7 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps return ( diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx index ba69db35766..9d4510a2ac1 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, ReactNode } from 'react'; import Skeleton from 'react-loading-skeleton'; import { useToggle } from 'react-use'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertingAPI, getContactPointDescription } from '@grafana/alerting/unstable'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -23,8 +24,10 @@ interface ContactPointGroupProps extends PropsWithChildren { export function GrafanaContactPointGroup({ name, matchedInstancesCount, children }: ContactPointGroupProps) { // find receiver by name – since this is what we store in the alert rule definition + const encodedName = base64UrlEncode(name); + const { data, isLoading } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: stringifyFieldSelector([['spec.title', name]]), + fieldSelector: stringifyFieldSelector([['metadata.name', encodedName]]), }); // grab the first result from the fieldSelector result diff --git a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx index b3c9196e664..741708860e0 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx @@ -1,9 +1,11 @@ import { ComponentProps } from 'react'; import Skeleton from 'react-loading-skeleton'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertingAPI } from '@grafana/alerting/unstable'; import { TextLink } from '@grafana/ui'; +import { stringifyFieldSelector } from '../../utils/k8s/utils'; import { makeEditContactPointLink } from '../../utils/misc'; interface ContactPointLinkProps extends Omit, 'href' | 'children'> { @@ -11,9 +13,11 @@ interface ContactPointLinkProps extends Omit, 'h } export const ContactPointLink = ({ name, ...props }: ContactPointLinkProps) => { - // find receiver by name – since this is what we store in the alert rule definition + const encodedName = base64UrlEncode(name); + + // find receiver by name using metadata.name field selector const { currentData, isLoading, isSuccess } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: `spec.title=${name}`, + fieldSelector: stringifyFieldSelector([['metadata.name', encodedName]]), }); // grab the first result from the fieldSelector result diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts index 126423077d7..84503c2ce13 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts @@ -1,9 +1,10 @@ import { HttpResponse, http } from 'msw'; +import { base64UrlEncode } from '@grafana/alerting'; import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; -import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; /** UID of a time interval that we expect to follow all happy paths within tests/mocks */ export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786'; @@ -20,9 +21,9 @@ const allTimeIntervals = getK8sResponse ); } - // Rudimentary filter support for `spec.name` + // Rudimentary filter support for `metadata.name` const url = new URL(request.url); const fieldSelector = url.searchParams.get('fieldSelector'); - if (fieldSelector && fieldSelector.includes('spec.name')) { + if (fieldSelector && fieldSelector.includes('metadata.name')) { const filteredItems = filterBySelector(allTimeIntervals.items, fieldSelector); return HttpResponse.json({ items: filteredItems }); diff --git a/public/app/features/alerting/unified/mocks/server/utils.ts b/public/app/features/alerting/unified/mocks/server/utils.ts index 5f01e038225..0ff0618a5f2 100644 --- a/public/app/features/alerting/unified/mocks/server/utils.ts +++ b/public/app/features/alerting/unified/mocks/server/utils.ts @@ -1,5 +1,6 @@ import { DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams } from 'msw'; +import { base64UrlEncode } from '@grafana/alerting'; import { PromRuleGroupDTO, PromRulesResponse } from 'app/types/unified-alerting-dto'; /** Helper method to help generate a kubernetes-style response with a list of items */ @@ -20,7 +21,7 @@ export function paginatedHandlerFor( ): HttpResponseResolver { const orderedGroupsWithCursor = groups.map((group) => ({ ...group, - id: Buffer.from(`${group.file}-${group.name}`).toString('base64url'), + id: base64UrlEncode(`${group.file}-${group.name}`), })); return ({ request }) => { diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index e75818cde66..8aecb9dd46b 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -52,5 +52,7 @@ export const encodeFieldSelector = (value: string): string => { type FieldSelector = [string, string] | [string, string, '=' | '!=']; export const stringifyFieldSelector = (fieldSelectors: FieldSelector[]): string => { - return fieldSelectors.map(([key, value, operator = '=']) => `${key}${operator}${value}`).join(','); + return fieldSelectors + .map(([key, value, operator = '=']) => `${key}${operator}${encodeFieldSelector(value)}`) + .join(','); }; From 02fb28a47886cae0748c5615af9f93a1e475926b Mon Sep 17 00:00:00 2001 From: Cory Forseth Date: Fri, 3 Oct 2025 10:22:19 -0500 Subject: [PATCH 07/19] AUTHZ: add option to bypass team membership cache (#111968) add option to bypass team membership cache --- pkg/services/team/team.go | 2 +- pkg/services/team/teamimpl/team.go | 16 ++++++++++------ pkg/services/team/teamtest/team.go | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 49db0c53734..8d55fb908a8 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -19,7 +19,7 @@ type Service interface { GetTeamIDsByUser(ctx context.Context, query *GetTeamIDsByUserQuery) ([]int64, error) IsTeamMember(ctx context.Context, orgId int64, teamId int64, userId int64) (bool, error) RemoveUsersMemberships(tx context.Context, userID int64) error - GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*TeamMemberDTO, error) + GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*TeamMemberDTO, error) GetTeamMembers(ctx context.Context, query *GetTeamMembersQuery) ([]*TeamMemberDTO, error) RegisterDelete(query string) } diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index 245ee98ddae..94fcf6d2186 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -114,18 +114,20 @@ func (s *Service) RemoveUsersMemberships(ctx context.Context, userID int64) erro return s.store.RemoveUsersMemberships(ctx, userID) } -func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) { +func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*team.TeamMemberDTO, error) { ctx, span := s.tracer.Start(ctx, "team.GetUserTeamMemberships", trace.WithAttributes( attribute.Int64("orgID", orgID), attribute.Int64("userID", userID), )) defer span.End() cacheKey := fmt.Sprintf("teams:%d:%d:%t", orgID, userID, external) - if cached, found := s.cache.Get(cacheKey); found { - if teams, ok := cached.([]*team.TeamMemberDTO); ok { - return teams, nil + if !bypassCache { + if cached, found := s.cache.Get(cacheKey); found { + if teams, ok := cached.([]*team.TeamMemberDTO); ok { + return teams, nil + } + s.cache.Delete(cacheKey) } - s.cache.Delete(cacheKey) } teams, err := s.store.GetMemberships(ctx, orgID, userID, external) if err != nil { @@ -137,7 +139,9 @@ func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int6 return []*team.TeamMemberDTO{}, nil } - s.cache.Set(cacheKey, teams, defaultCacheDuration) + if !bypassCache { + s.cache.Set(cacheKey, teams, defaultCacheDuration) + } return teams, nil } diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 3caefb9562e..3b88d05d5d3 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -58,7 +58,7 @@ func (s *FakeService) RemoveUsersMemberships(ctx context.Context, userID int64) return s.ExpectedError } -func (s *FakeService) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) { +func (s *FakeService) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*team.TeamMemberDTO, error) { return s.ExpectedMembers, s.ExpectedError } From 99784bea14f37919485733c91428c58806c64618 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 3 Oct 2025 16:28:51 +0100 Subject: [PATCH 08/19] Plugins: Add toggle for plugin CDN URLs (#111985) add toggle --- .../grafana-data/src/types/featureToggles.gen.ts | 5 +++++ pkg/services/featuremgmt/registry.go | 8 ++++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 13 +++++++++++++ 5 files changed, 31 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 169d72c20fc..21fe50934cb 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1202,4 +1202,9 @@ export interface FeatureToggles { * @default false */ cdnPluginsLoadFirst?: boolean; + /** + * Enable loading plugins via declarative URLs + * @default false + */ + cdnPluginsUrls?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 98e838d9318..9be34ab40c1 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2084,6 +2084,14 @@ var ( Owner: grafanaPluginsPlatformSquad, Expression: "false", }, + { + Name: "cdnPluginsUrls", + Description: "Enable loading plugins via declarative URLs", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaPluginsPlatformSquad, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b453c49da28..5f8f337c885 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -267,3 +267,4 @@ pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,fal tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false filterOutBotsFromFrontendLogs,experimental,@grafana/plugins-platform-backend,false,false,true cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false +cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 501d47de58b..cd03ada2338 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -1077,4 +1077,8 @@ const ( // FlagCdnPluginsLoadFirst // Prioritize loading plugins from the CDN before other sources FlagCdnPluginsLoadFirst = "cdnPluginsLoadFirst" + + // FlagCdnPluginsUrls + // Enable loading plugins via declarative URLs + FlagCdnPluginsUrls = "cdnPluginsUrls" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 264edb84fab..38b80c8554b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -903,6 +903,19 @@ "expression": "false" } }, + { + "metadata": { + "name": "cdnPluginsUrls", + "resourceVersion": "1759489886228", + "creationTimestamp": "2025-10-03T11:11:26Z" + }, + "spec": { + "description": "Enable loading plugins via declarative URLs", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "expression": "false" + } + }, { "metadata": { "name": "cloudRBACRoles", From 15ec2f0c171a5178466e7044f79b68c516914237 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 3 Oct 2025 16:38:31 +0100 Subject: [PATCH 09/19] Internationalisation: Allow parameterising `basePaths` for key generation in lint rule (#111999) * allow parameterising the basePaths for autofixes * add documentation --- eslint.config.js | 5 +- packages/grafana-i18n/src/eslint/README.md | 24 +++++ .../no-untranslated-strings.cjs | 13 ++- .../no-untranslated-strings.test.js | 95 ++++++++++++++++++- .../translation-utils.cjs | 11 ++- 5 files changed, 136 insertions(+), 12 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 4aa4e15a0f9..a0245a55b48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -361,7 +361,10 @@ module.exports = [ '**/mock*.{ts,tsx}', ], rules: { - '@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }], + '@grafana/i18n/no-untranslated-strings': [ + 'error', + { calleesToIgnore: ['^css$', 'use[A-Z].*'], basePaths: ['public/app/features'] }, + ], '@grafana/i18n/no-translation-top-level': 'error', }, }, diff --git a/packages/grafana-i18n/src/eslint/README.md b/packages/grafana-i18n/src/eslint/README.md index 4fb1abdf8f6..12b3a559f9b 100644 --- a/packages/grafana-i18n/src/eslint/README.md +++ b/packages/grafana-i18n/src/eslint/README.md @@ -10,6 +10,30 @@ Check if strings are marked for translation inside JSX Elements, in certain JSX ### Options +#### `basePaths` + +Allows specifying base paths that should be stripped when generating i18n keys. Defaults to `['src']`. + +#### Example + +```tsx +// For a file located at public/app/features/search/EmptyState.tsx + +// Specifying basePaths: +// { +// '@grafana/i18n/no-untranslated-strings': ['error', { basePaths: ['public/app/features'] }], +// } + +No results found + +// Without basePaths: +// { +// '@grafana/i18n/no-untranslated-strings': ['error'], +// } + +No results found +``` + #### `forceFix` Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves. diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs index 7d741720bc2..2b058426d53 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs @@ -2,8 +2,8 @@ /** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */ -/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[] }]>} RuleDefinition */ -/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[]}]>} RuleContextWithOptions */ +/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[], basePaths: string[] }]>} RuleDefinition */ +/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */ const { getNodeValue, @@ -301,12 +301,19 @@ const noUntranslatedStrings = createRule({ }, default: [], }, + basePaths: { + type: 'array', + items: { + type: 'string', + }, + default: ['src'], + }, }, additionalProperties: false, }, ], }, - defaultOptions: [{ forceFix: [], calleesToIgnore: [] }], + defaultOptions: [{ forceFix: [], calleesToIgnore: [], basePaths: ['src'] }], }); module.exports = noUntranslatedStrings; diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js index 9913ab10f73..684823ae152 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js @@ -2,7 +2,7 @@ import { RuleTester } from 'eslint'; import noUntranslatedStrings from './no-untranslated-strings.cjs'; -const filename = 'public/app/features/some-feature/nested/SomeFile.tsx'; +const filename = 'src/some-feature/nested/SomeFile.tsx'; const packageName = '@grafana/i18n'; @@ -797,7 +797,7 @@ const Foo = () => { name: 'Auto fixes when options are configured', code: `const Foo = () =>
test
`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], output: `${TRANS_IMPORT} const Foo = () =>
test
`, errors: [ @@ -821,7 +821,7 @@ const Foo = () => { return
}`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], output: ` ${T_IMPORT} const Foo = () => { @@ -853,7 +853,94 @@ const Foo = () => { } }`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], + output: ` +${T_IMPORT} +const Foo = () => { + return { + label: t("some-feature.foo.label.test", "test"), + } +}`, + errors: [ + { + messageId: 'noUntranslatedStringsProperties', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return { + label: t("some-feature.foo.label.test", "test"), + } +}`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes when options are configured for a different basePath', + code: `const Foo = () =>
test
`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], + output: `${TRANS_IMPORT} +const Foo = () =>
test
`, + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: `${TRANS_IMPORT} +const Foo = () =>
test
`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes when options are configured for a different basePath - prop', + code: ` +const Foo = () => { + return
+}`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], + output: ` +${T_IMPORT} +const Foo = () => { + return
+}`, + errors: [ + { + messageId: 'noUntranslatedStringsProp', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return
+}`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes object property for a different basePath', + code: ` +const Foo = () => { + return { + label: 'test', + } +}`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], output: ` ${T_IMPORT} const Foo = () => { diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs index 070e0c5ee53..8d7d4456ace 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs @@ -7,7 +7,7 @@ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */ /** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */ /** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */ -/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[]}]>} RuleContextWithOptions */ +/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */ const { AST_NODE_TYPES } = require('@typescript-eslint/utils'); /** @@ -150,9 +150,12 @@ function getTDeclaration(node, context) { */ function getTranslationPrefix(context) { const filename = context.filename; - const match = filename.match(/public\/app\/features\/(.+?)\//); - if (match) { - return match[1]; + const basePaths = context.options[0]?.basePaths ?? ['src']; + for (const path of basePaths) { + const match = filename.match(new RegExp(`${path}/(.+?)/`)); + if (match) { + return match[1]; + } } return null; } From e414e790f7410d22b1c337be7d4b7c2473ffd039 Mon Sep 17 00:00:00 2001 From: djpnicholls <109083091+djpnicholls@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:43:51 +0100 Subject: [PATCH 10/19] Alerting Provisioning: Don't error on recording rules without conditions (#109410) * Don't error on recording rules without conditions The provisioning model doesn't include conditions for recording rules. Only return an error for a missing condition if the rule isn't a recording rule. Also, added a test case to show the failure. This resolves #109398 * Run `go fmt` to fix whitespace issues --- .../provisioning/alerting/rules_types.go | 8 ++-- .../provisioning/alerting/rules_types_test.go | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/rules_types.go index b5b28072beb..bac091ababa 100644 --- a/pkg/services/provisioning/alerting/rules_types.go +++ b/pkg/services/provisioning/alerting/rules_types.go @@ -151,10 +151,6 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { noDataState = models.NoData } alertRule.NoDataState = noDataState - alertRule.Condition = rule.Condition.Value() - if alertRule.Condition == "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) - } alertRule.Annotations = rule.Annotations.Raw alertRule.Labels = rule.Labels.Value() for _, queryV1 := range rule.Data { @@ -182,6 +178,10 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { } alertRule.Record = &record } + alertRule.Condition = rule.Condition.Value() + if alertRule.Condition == "" && alertRule.Record == nil { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) + } return alertRule, nil } diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/rules_types_test.go index 16c510707b9..9c4e819ff73 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/rules_types_test.go @@ -202,6 +202,14 @@ func TestRules(t *testing.T) { }) } +func TestRecordingRules(t *testing.T) { + t.Run("a valid rule should not error", func(t *testing.T) { + rule := validRecordingRuleV1(t) + _, err := rule.mapToModel(1) + require.NoError(t, err) + }) +} + func TestNotificationsSettingsV1MapToModel(t *testing.T) { tests := []struct { name string @@ -347,6 +355,37 @@ func validRuleV1(t *testing.T) AlertRuleV1 { } } +func validRecordingRuleV1(t *testing.T) AlertRuleV1 { + t.Helper() + var ( + title values.StringValue + uid values.StringValue + forDuration values.StringValue + metric values.StringValue + from values.StringValue + ) + err := yaml.Unmarshal([]byte("test"), &title) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("test_uid"), &uid) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("10s"), &forDuration) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("test_metric"), &metric) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("A"), &from) + require.NoError(t, err) + return AlertRuleV1{ + Title: title, + UID: uid, + For: forDuration, + Record: &RecordV1{ + Metric: metric, + From: from, + }, + Data: []QueryV1{{}}, + } +} + func stringToStringValue(s string) values.StringValue { result := values.StringValue{} err := yaml.Unmarshal([]byte(s), &result) From 2424b1cee9ee235fb009ec730f07eb1c7f6b74d5 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 3 Oct 2025 17:35:28 +0100 Subject: [PATCH 11/19] Slider: Make `inputId` a required param and fix minor a11y violations (#112006) * make inputId a required param for , fix a11y violations * differentiate between slider and rangeslider --- eslint-suppressions.json | 10 ---------- .../src/components/Slider/RangeSlider.story.tsx | 2 -- .../src/components/Slider/RangeSlider.tsx | 4 ++++ .../src/components/Slider/Slider.story.tsx | 17 +++++++++++++---- .../src/components/Slider/Slider.test.tsx | 1 + .../grafana-ui/src/components/Slider/Slider.tsx | 8 +++++++- .../grafana-ui/src/components/Slider/types.ts | 1 + public/locales/en-US/grafana.json | 6 ++++++ 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 43845407d57..227edc2fd71 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -833,16 +833,6 @@ "count": 13 } }, - "packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/Slider/Slider.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx index d00648bdfbe..eccb916483a 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx @@ -9,8 +9,6 @@ const meta: Meta = { controls: { exclude: ['tooltipAlwaysVisible'], }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx index 34b9d403085..794ea5b795f 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx @@ -3,6 +3,8 @@ import { Global } from '@emotion/react'; import Slider, { SliderProps } from 'rc-slider'; import { useCallback } from 'react'; +import { t } from '@grafana/i18n'; + import { useStyles2 } from '../../themes/ThemeContext'; import HandleTooltip from './HandleTooltip'; @@ -44,6 +46,7 @@ export const RangeSlider = ({ const isHorizontal = orientation === 'horizontal'; const styles = useStyles2(getStyles, isHorizontal); + const dragHandleAriaLabel = t('grafana-ui.range-slider.drag-handle-aria-label', 'Use arrow keys to change the value'); const tipHandleRender: SliderProps['handleRender'] = (node, handleProps) => { return ( @@ -73,6 +76,7 @@ export const RangeSlider = ({ vertical={!isHorizontal} reverse={reverse} handleRender={tipHandleRender} + ariaLabelForHandle={dragHandleAriaLabel} />
); diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx index 770038dbc64..207a33ec5fe 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -1,4 +1,7 @@ import { StoryFn, Meta } from '@storybook/react'; +import { useId } from 'react'; + +import { Field } from '../Forms/Field'; import { Slider } from './Slider'; @@ -12,8 +15,6 @@ const meta: Meta = { knobs: { disabled: true, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, @@ -31,17 +32,25 @@ const meta: Meta = { }; export const Basic: StoryFn = (args) => { + const id = useId(); + return (
- + + +
); }; export const WithMarks: StoryFn = (args) => { + const id = useId(); + return (
- + + +
); }; diff --git a/packages/grafana-ui/src/components/Slider/Slider.test.tsx b/packages/grafana-ui/src/components/Slider/Slider.test.tsx index 2723a180b3c..e24ccd7e751 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.test.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.test.tsx @@ -7,6 +7,7 @@ import { SliderProps } from './types'; const sliderProps: SliderProps = { min: 10, max: 20, + inputId: 'slider-test', }; describe('Slider', () => { diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index c677d402dc9..e8c377070bf 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -3,6 +3,8 @@ import { Global } from '@emotion/react'; import SliderComponent from 'rc-slider'; import { useState, useCallback, ChangeEvent, FocusEvent } from 'react'; +import { t } from '@grafana/i18n'; + import { useStyles2 } from '../../themes/ThemeContext'; import { Input } from '../Input/Input'; @@ -24,11 +26,14 @@ export const Slider = ({ ariaLabelForHandle, marks, included, + inputId, }: SliderProps) => { const isHorizontal = orientation === 'horizontal'; const styles = useStyles2(getStyles, isHorizontal, Boolean(marks)); const SliderWithTooltip = SliderComponent; const [sliderValue, setSliderValue] = useState(value ?? min); + const dragHandleAriaLabel = + ariaLabelForHandle ?? t('grafana-ui.slider.drag-handle-aria-label', 'Use arrow keys to change the value'); const onSliderChange = useCallback( (v: number | number[]) => { @@ -102,7 +107,7 @@ export const Slider = ({ onChangeComplete={handleChangeComplete} vertical={!isHorizontal} reverse={reverse} - ariaLabelForHandle={ariaLabelForHandle} + ariaLabelForHandle={dragHandleAriaLabel} marks={marks} included={included} /> @@ -116,6 +121,7 @@ export const Slider = ({ onBlur={onSliderInputBlur} min={min} max={max} + id={inputId} />
diff --git a/packages/grafana-ui/src/components/Slider/types.ts b/packages/grafana-ui/src/components/Slider/types.ts index 2551ac84937..812a8bed7d7 100644 --- a/packages/grafana-ui/src/components/Slider/types.ts +++ b/packages/grafana-ui/src/components/Slider/types.ts @@ -21,6 +21,7 @@ export interface SliderProps extends CommonSliderProps { onAfterChange?: (value?: number) => void; formatTooltipResult?: (value: number) => number; ariaLabelForHandle?: string; + inputId: string; } export interface RangeSliderProps extends CommonSliderProps { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d976f91085a..ed29beb6afe 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8697,6 +8697,9 @@ "tooltip-hide": "Hide password", "tooltip-show": "Show password" }, + "range-slider": { + "drag-handle-aria-label": "Use arrow keys to change the value" + }, "row-expander": { "aria-label-expand": "Expand row", "collapse": "Collapse row", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Use right y-axis" }, + "slider": { + "drag-handle-aria-label": "Use arrow keys to change the value" + }, "spinner": { "aria-label": "Loading" }, From 0c0c66fda10c6efa43739adf13be3becaee65527 Mon Sep 17 00:00:00 2001 From: Daniele Stefano Ferru Date: Fri, 3 Oct 2025 20:49:44 +0200 Subject: [PATCH 12/19] Provisioning: update unhealthy status only when not recent (#112014) --- .../apis/provisioning/controller/health.go | 19 +++++++-- .../provisioning/controller/health_test.go | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/pkg/registry/apis/provisioning/controller/health.go b/pkg/registry/apis/provisioning/controller/health.go index 5e5953b4117..ed5f19c6ed0 100644 --- a/pkg/registry/apis/provisioning/controller/health.go +++ b/pkg/registry/apis/provisioning/controller/health.go @@ -12,6 +12,13 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +const ( + // recentHealthyDuration defines how recent a health check must be to be considered "recent" when healthy + recentHealthyDuration = 5 * time.Minute + // recentHealthyDuration defines how recent a health check must be to be considered "recent" when unhealthy + recentUnhealthyDuration = 1 * time.Minute +) + // StatusPatcher defines the interface for updating repository status // //go:generate mockery --name=StatusPatcher @@ -60,9 +67,9 @@ func (hc *HealthChecker) hasRecentHealthCheck(healthStatus provisioning.HealthSt age := time.Since(time.UnixMilli(healthStatus.Checked)) if healthStatus.Healthy { - return age <= time.Minute*5 // Recent if checked within 5 minutes when healthy + return age <= recentHealthyDuration } - return age <= time.Minute // Recent if checked within 1 minute when unhealthy + return age <= recentUnhealthyDuration // Recent if checked within 1 minute when unhealthy } // HasRecentFailure checks if there's a recent failure of a specific type @@ -72,7 +79,7 @@ func (hc *HealthChecker) HasRecentFailure(healthStatus provisioning.HealthStatus } age := time.Since(time.UnixMilli(healthStatus.Checked)) - return age <= time.Minute // Recent if within 1 minute + return age <= recentUnhealthyDuration } // RecordFailureAndUpdate records a failure and updates the repository status @@ -111,7 +118,11 @@ func (hc *HealthChecker) hasHealthStatusChanged(old, new provisioning.HealthStat return true } - if old.Checked != new.Checked { + recent := recentUnhealthyDuration + if new.Healthy { + recent = recentHealthyDuration + } + if time.UnixMilli(new.Checked).Sub(time.UnixMilli(old.Checked)) > recent { return true } diff --git a/pkg/registry/apis/provisioning/controller/health_test.go b/pkg/registry/apis/provisioning/controller/health_test.go index 722500d5a94..91a0a67a131 100644 --- a/pkg/registry/apis/provisioning/controller/health_test.go +++ b/pkg/registry/apis/provisioning/controller/health_test.go @@ -400,6 +400,26 @@ func TestRefreshHealth(t *testing.T) { expectedHealth: true, expectPatch: false, }, + { + name: "no status change - no patch needed for unhealthy repo (recent check)", + testResult: &provisioning.TestResults{ + Success: false, + Code: 500, + Errors: []provisioning.ErrorDetails{ + {Detail: "connection failed"}, + {Detail: "timeout"}, + }, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-15 * time.Second).UnixMilli(), + Message: []string{"connection failed", "timeout"}, + }, + expectError: false, + expectedHealth: false, + expectPatch: false, + }, { name: "status unchanged but timestamp needs update (old check)", testResult: &provisioning.TestResults{ @@ -415,6 +435,26 @@ func TestRefreshHealth(t *testing.T) { expectedHealth: true, expectPatch: true, }, + { + name: "status unchanged but timestamp needs update (old unhealthy check)", + testResult: &provisioning.TestResults{ + Success: false, + Code: 500, + Errors: []provisioning.ErrorDetails{ + {Detail: "connection failed"}, + {Detail: "timeout"}, + }, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-2 * time.Minute).UnixMilli(), + Message: []string{"connection failed", "timeout"}, + }, + expectError: false, + expectedHealth: false, + expectPatch: true, + }, { name: "patch error", testResult: &provisioning.TestResults{ From acad92864ecb83a9cd8a9b6a4680e72db5bdb437 Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:25:46 -0400 Subject: [PATCH 13/19] Secrets: Refactor data_key_id out of the encoded secure value payload (#111852) * everything compiles * tests pass * remove file included by accident * add entry to gitignore * some scaffolding for the migration executor * remove file * implement and test the migration * use xkube.Namespace in our interfaces * add todo * update wire deps * add some logs * fix wire dependency ordering * create tests to validate error conditions during migrations --- .gitignore | 4 +- .../apis/secret/contracts/encryption.go | 40 +- pkg/registry/apis/secret/contracts/keeper.go | 8 +- .../apis/secret/encryption/manager/manager.go | 78 +--- .../secret/encryption/manager/manager_test.go | 33 +- .../secret/garbagecollectionworker/worker.go | 2 +- .../garbagecollectionworker/worker_test.go | 5 +- .../apis/secret/secretkeeper/secretkeeper.go | 13 +- .../secret/secretkeeper/secretkeeper_test.go | 4 +- .../secret/secretkeeper/sqlkeeper/keeper.go | 36 +- .../secretkeeper/sqlkeeper/keeper_test.go | 5 +- .../apis/secret/service/consolidation.go | 7 +- .../apis/secret/service/consolidation_test.go | 12 +- .../apis/secret/service/secure_value.go | 4 +- .../apis/secret/testutils/testutils.go | 76 +-- pkg/server/wire.go | 1 + pkg/server/wire_gen.go | 52 ++- .../data/encrypted_value_create.sql | 2 + .../data/encrypted_value_list_all.sql | 1 + .../encryption/data/encrypted_value_read.sql | 1 + .../data/encrypted_value_update.sql | 1 + .../encryption/encrypted_value_model.go | 1 + .../encryption/encrypted_value_store.go | 155 +++++-- .../encryption/encrypted_value_store_test.go | 433 ++++++++++++++++-- pkg/storage/secret/encryption/query.go | 1 + pkg/storage/secret/encryption/query_test.go | 2 + .../mysql--encrypted_value_create-create.sql | 2 + ...sql--encrypted_value_list_all-list_all.sql | 1 + ...ted_value_list_all-list_all_until_time.sql | 1 + ..._value_list_all-list_limit_10_offset_0.sql | 1 + ..._value_list_all-list_limit_10_offset_2.sql | 1 + .../mysql--encrypted_value_read-read.sql | 1 + .../mysql--encrypted_value_update-update.sql | 1 + ...ostgres--encrypted_value_create-create.sql | 2 + ...res--encrypted_value_list_all-list_all.sql | 1 + ...ted_value_list_all-list_all_until_time.sql | 1 + ..._value_list_all-list_limit_10_offset_0.sql | 1 + ..._value_list_all-list_limit_10_offset_2.sql | 1 + .../postgres--encrypted_value_read-read.sql | 1 + ...ostgres--encrypted_value_update-update.sql | 1 + .../sqlite--encrypted_value_create-create.sql | 2 + ...ite--encrypted_value_list_all-list_all.sql | 1 + ...ted_value_list_all-list_all_until_time.sql | 1 + ..._value_list_all-list_limit_10_offset_0.sql | 1 + ..._value_list_all-list_limit_10_offset_2.sql | 1 + .../sqlite--encrypted_value_read-read.sql | 1 + .../sqlite--encrypted_value_update-update.sql | 1 + pkg/storage/secret/metadata/decrypt_store.go | 2 +- pkg/storage/secret/migrator/migrator.go | 11 + 49 files changed, 782 insertions(+), 232 deletions(-) diff --git a/.gitignore b/.gitignore index fb7d5d30a9a..d6dd8fb6d83 100644 --- a/.gitignore +++ b/.gitignore @@ -250,6 +250,8 @@ public/mockServiceWorker.js /e2e-playwright/test-plugins/*/dist /apps/provisioning/cmd/job-controller/bin/ - # Ignore unified storage kv store files /grafana-kv-data + +# Ignore debug output from test library +/pkg/storage/secret/metadata/testdata/rapid/TestStateMachine/ diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index f24fe73a544..73a4952cac2 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -1,6 +1,10 @@ package contracts -import "context" +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) // EncryptionManager is an envelope encryption service in charge of encrypting/decrypting secrets. type EncryptionManager interface { @@ -8,17 +12,23 @@ type EncryptionManager interface { // For those specific use cases where the encryption operation cannot be moved outside // the database transaction, look at database-specific methods present at the specific // implementation present at manager.EncryptionService. - Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) - Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) + Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (EncryptedPayload, error) + Decrypt(ctx context.Context, namespace xkube.Namespace, payload EncryptedPayload) ([]byte, error) +} + +type EncryptedPayload struct { + DataKeyID string + EncryptedData []byte } type EncryptedValue struct { - Namespace string - Name string - Version int64 - EncryptedData []byte - Created int64 - Updated int64 + EncryptedPayload + + Namespace string + Name string + Version int64 + Created int64 + Updated int64 } // ListOpts defines pagination options for listing encrypted values. @@ -28,10 +38,10 @@ type ListOpts struct { } type EncryptedValueStorage interface { - Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (*EncryptedValue, error) - Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error - Get(ctx context.Context, namespace, name string, version int64) (*EncryptedValue, error) - Delete(ctx context.Context, namespace, name string, version int64) error + Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) (*EncryptedValue, error) + Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) error + Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*EncryptedValue, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error } type GlobalEncryptedValueStorage interface { @@ -39,6 +49,10 @@ type GlobalEncryptedValueStorage interface { CountAll(ctx context.Context, untilTime *int64) (int64, error) } +type EncryptedValueMigrationExecutor interface { + Execute(ctx context.Context) (int, error) +} + type ConsolidationService interface { Consolidate(ctx context.Context) error } diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index 5558a4db3b5..a204bb5b1a7 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -96,10 +96,10 @@ func (s ExternalID) String() string { // Keeper is the interface for secret keepers. type Keeper interface { - Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) - Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error - Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) - Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error + Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) + Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error + Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) + Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error } // Service is the interface for secret keeper services. diff --git a/pkg/registry/apis/secret/encryption/manager/manager.go b/pkg/registry/apis/secret/encryption/manager/manager.go index f48b350c1bd..024c8aca05a 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager.go +++ b/pkg/registry/apis/secret/encryption/manager/manager.go @@ -1,10 +1,8 @@ package manager import ( - "bytes" "context" "crypto/rand" - "encoding/base64" "errors" "fmt" "strconv" @@ -20,13 +18,10 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/util" ) -const ( - keyIdDelimiter = '#' -) - type EncryptionManager struct { tracer trace.Tracer store contracts.DataKeyStorage @@ -99,12 +94,9 @@ func (s *EncryptionManager) registerUsageMetrics() { }) } -// TODO: Why do we need to use a global variable for this? -var b64 = base64.RawStdEncoding - -func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (contracts.EncryptedPayload, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Encrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -128,34 +120,30 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo id, dataKey, err = s.currentDataKey(ctx, namespace, label) if err != nil { s.log.Error("Failed to get current data key", "error", err, "label", label) - return nil, err + return contracts.EncryptedPayload{}, err } var encrypted []byte encrypted, err = s.cipher.Encrypt(ctx, payload, string(dataKey)) if err != nil { s.log.Error("Failed to encrypt secret", "error", err) - return nil, err + return contracts.EncryptedPayload{}, err } - prefix := make([]byte, b64.EncodedLen(len(id))+2) - b64.Encode(prefix[1:], []byte(id)) - prefix[0] = keyIdDelimiter - prefix[len(prefix)-1] = keyIdDelimiter + encryptedPayload := contracts.EncryptedPayload{ + DataKeyID: id, + EncryptedData: encrypted, + } - blob := make([]byte, len(prefix)+len(encrypted)) - copy(blob, prefix) - copy(blob[len(prefix):], encrypted) - - return blob, nil + return encryptedPayload, nil } // currentDataKey looks up for current data key in cache or database by name, and decrypts it. // If there's no current data key in cache nor in database it generates a new random data key, // and stores it into both the in-memory cache and database (encrypted by the encryption provider). -func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string, label string) (string, []byte, error) { +func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace xkube.Namespace, label string) (string, []byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.CurrentDataKey", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("label", label), )) defer span.End() @@ -166,14 +154,14 @@ func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string defer s.mtx.Unlock() // We try to fetch the data key, either from cache or database - id, dataKey, err := s.dataKeyByLabel(ctx, namespace, label) + id, dataKey, err := s.dataKeyByLabel(ctx, namespace.String(), label) if err != nil { return "", nil, err } // If no existing data key was found, create a new one if dataKey == nil { - id, dataKey, err = s.newDataKey(ctx, namespace, label) + id, dataKey, err = s.newDataKey(ctx, namespace.String(), label) if err != nil { return "", nil, err } @@ -264,9 +252,9 @@ func newRandomDataKey() ([]byte, error) { return rawDataKey, nil } -func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Decrypt(ctx context.Context, namespace xkube.Namespace, payload contracts.EncryptedPayload) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Decrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -285,50 +273,28 @@ func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, paylo } }() - if len(payload) == 0 { + if len(payload.EncryptedData) == 0 { err = fmt.Errorf("unable to decrypt empty payload") return nil, err } - payload = payload[1:] - endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) - if endOfKey == -1 { - err = fmt.Errorf("could not find valid key id in encrypted payload") - return nil, err - } - b64Key := payload[:endOfKey] - payload = payload[endOfKey+1:] - keyId := make([]byte, b64.DecodedLen(len(b64Key))) - _, err = b64.Decode(keyId, b64Key) - if err != nil { + if payload.DataKeyID == "" { + err = fmt.Errorf("unable to decrypt empty data key id") return nil, err } - dataKey, err := s.dataKeyById(ctx, namespace, string(keyId)) + dataKey, err := s.dataKeyById(ctx, namespace.String(), payload.DataKeyID) if err != nil { - s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", string(keyId), "error", err) + s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", payload.DataKeyID, "error", err) return nil, err } var decrypted []byte - decrypted, err = s.cipher.Decrypt(ctx, payload, string(dataKey)) + decrypted, err = s.cipher.Decrypt(ctx, payload.EncryptedData, string(dataKey)) return decrypted, err } -func (s *EncryptionManager) GetDecryptedValue(ctx context.Context, namespace string, sjd map[string][]byte, key, fallback string) string { - if value, ok := sjd[key]; ok { - decryptedData, err := s.Decrypt(ctx, namespace, value) - if err != nil { - return fallback - } - - return string(decryptedData) - } - - return fallback -} - // dataKeyById looks up for data key in the database and returns it decrypted. func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id string) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.GetDataKey", trace.WithAttributes( diff --git a/pkg/registry/apis/secret/encryption/manager/manager_test.go b/pkg/registry/apis/secret/encryption/manager/manager_test.go index a6c1c06bcfa..dc63fb461c9 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager_test.go +++ b/pkg/registry/apis/secret/encryption/manager/manager_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -34,7 +35,7 @@ func TestMain(m *testing.M) { func TestEncryptionService_EnvelopeEncryption(t *testing.T) { svc := setupTestService(t) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("encrypting should create DEK", func(t *testing.T) { plaintext := []byte("very secret string") @@ -46,7 +47,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -61,7 +62,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -212,7 +213,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { } encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1") - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{}) assert.True(t, fake.encryptCalled) assert.False(t, fake.decryptCalled) @@ -241,7 +242,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Generate random keys for testing oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars @@ -416,16 +417,30 @@ func (p *fakeProvider) Decrypt(_ context.Context, _ []byte) ([]byte, error) { func TestEncryptionService_Decrypt(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("empty payload should fail", func(t *testing.T) { svc := setupTestService(t) - _, err := svc.Decrypt(context.Background(), namespace, []byte("")) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte(""), + }) require.Error(t, err) assert.Equal(t, "unable to decrypt empty payload", err.Error()) }) + t.Run("empty data key id should fail", func(t *testing.T) { + svc := setupTestService(t) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "", + EncryptedData: []byte("some payload"), + }) + require.Error(t, err) + + assert.Equal(t, "unable to decrypt empty data key id", err.Error()) + }) + t.Run("ee encrypted payload with ee enabled should work", func(t *testing.T) { svc := setupTestService(t) ciphertext, err := svc.Encrypt(ctx, namespace, []byte("grafana")) @@ -442,7 +457,7 @@ func TestIntegration_SecretsService(t *testing.T) { ctx := context.Background() someData := []byte(`some-data`) - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") tcs := map[string]func(*testing.T, db.DB, contracts.EncryptionManager){ "regular": func(t *testing.T, _ db.DB, svc contracts.EncryptionManager) { @@ -562,7 +577,7 @@ func TestIntegration_SecretsService(t *testing.T) { require.NoError(t, err) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Here's what actually matters and varies on each test: look at the test case name. // diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker.go b/pkg/registry/apis/secret/garbagecollectionworker/worker.go index 4754f06c02d..e10bbed600e 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker.go @@ -104,7 +104,7 @@ func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) err } // Keeper deletion is idempotent - if err := keeper.Delete(ctx, keeperCfg, sv.Namespace, sv.Name, sv.Status.Version); err != nil { + if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil { return fmt.Errorf("deleting secure value from keeper: %w", err) } diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 344d2bcf605..23467d69b39 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -9,6 +9,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/mitchellh/copystructure" "github.com/stretchr/testify/require" @@ -58,7 +59,7 @@ func TestBasic(t *testing.T) { require.NoError(t, err) // Get the secret value once to make sure it's reachable - exposedValue, err := keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err := keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.NoError(t, err) require.NotEmpty(t, exposedValue.DangerouslyExposeAndConsumeValue()) @@ -78,7 +79,7 @@ func TestBasic(t *testing.T) { require.Empty(t, svs) // Try to get the secreet value again to make sure it's been deleted from the keeper - exposedValue, err = keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err = keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.ErrorIs(t, err, encryption.ErrEncryptedValueNotFound) require.Empty(t, exposedValue) }) diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go index fc65573ac61..dc51d5b3160 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go @@ -1,9 +1,12 @@ package secretkeeper import ( + "fmt" + "go.opentelemetry.io/otel/trace" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" "github.com/prometheus/client_golang/prometheus" @@ -20,11 +23,17 @@ func ProvideService( tracer trace.Tracer, store contracts.EncryptedValueStorage, encryptionManager contracts.EncryptionManager, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, + _ *secret.DependencyRegisterer, // noop import so wire runs DB migrations before instantiating this service -- can be nil when manually instantiating ) (*OSSKeeperService, error) { + systemKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, migrationExecutor, reg) + if err != nil { + return nil, fmt.Errorf("failed to create system keeper: %w", err) + } + return &OSSKeeperService{ - // TODO: rename to system keeper or something like that - systemKeeper: sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, reg), + systemKeeper: systemKeeper, }, nil } diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go index 84baaf207b0..43193befcea 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -12,6 +12,7 @@ import ( osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -65,7 +66,8 @@ func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error) require.NoError(t, err) // Initialize the keeper service - keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, nil) + keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, &testutils.NoopMigrationExecutor{}, nil, nil) + require.NoError(t, err) return keeperService, err } diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go index bfe3972f72b..a6f54d5674a 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -5,9 +5,11 @@ import ( "fmt" "time" + "github.com/grafana/grafana-app-sdk/logging" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/metrics" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -26,20 +28,32 @@ func NewSQLKeeper( tracer trace.Tracer, encryptionManager contracts.EncryptionManager, store contracts.EncryptedValueStorage, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, -) *SQLKeeper { +) (*SQLKeeper, error) { + // Run the encrypted value store migration before anything else, otherwise operations may fail + // TODO: This does not need to be here forever, but we may currently have on-prem deployments using GSM, so it needs to be here for now. + // Periodically assess whether it is safe to remove - most likely for G13 should be fine. + log := logging.FromContext(context.Background()) + log.Debug("sqlkeeper: executing encrypted value store migration") + rowsAffected, err := migrationExecutor.Execute(context.Background()) + log.Debug("sqlkeeper: encrypted value store migration completed", "rows_affected", rowsAffected) + if err != nil { + return nil, fmt.Errorf("error encountered during encrypted value store migration: %w", err) + } + return &SQLKeeper{ tracer: tracer, encryptionManager: encryptionManager, store: store, metrics: metrics.NewKeeperMetrics(reg), - } + }, nil } -func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { +func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version)), ) @@ -63,9 +77,9 @@ func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, n return contracts.ExternalID(""), nil } -func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { +func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -77,7 +91,7 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return "", fmt.Errorf("unable to get encrypted value: %w", err) } - exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedData) + exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedPayload) if err != nil { return "", fmt.Errorf("unable to decrypt value: %w", err) } @@ -88,9 +102,9 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return exposedValue, nil } -func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error { +func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,9 +121,9 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, return nil } -func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error { +func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index 1333cf894c7..db5139c50ff 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -8,6 +8,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -16,10 +17,10 @@ func TestMain(m *testing.M) { } func Test_SQLKeeperSetup(t *testing.T) { - namespace1 := "namespace1" + namespace1 := xkube.Namespace("namespace1") name1 := "name1" version1 := int64(1) - namespace2 := "namespace2" + namespace2 := xkube.Namespace("namespace2") name2 := "name2" plaintext1 := "very secret string in namespace 1" plaintext2 := "very secret string in namespace 2" diff --git a/pkg/registry/apis/secret/service/consolidation.go b/pkg/registry/apis/secret/service/consolidation.go index b0baea659c4..bbe7d9084e6 100644 --- a/pkg/registry/apis/secret/service/consolidation.go +++ b/pkg/registry/apis/secret/service/consolidation.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -60,21 +61,21 @@ func (s *ConsolidationService) Consolidate(ctx context.Context) (err error) { for _, ev := range encryptedValues { // Decrypt the value using its old data key. - decryptedValue, err := s.encryptionManager.Decrypt(ctx, ev.Namespace, ev.EncryptedData) + decryptedValue, err := s.encryptionManager.Decrypt(ctx, xkube.Namespace(ev.Namespace), ev.EncryptedPayload) if err != nil { logging.FromContext(ctx).Error("Failed to decrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Re-encrypt the value using a new data key. - reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, ev.Namespace, decryptedValue) + reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, xkube.Namespace(ev.Namespace), decryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to re-encrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Update the encrypted value in the store. - err = s.encryptedValueStore.Update(ctx, ev.Namespace, ev.Name, ev.Version, reEncryptedValue) + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(ev.Namespace), ev.Name, ev.Version, reEncryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to update encrypted value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue diff --git a/pkg/registry/apis/secret/service/consolidation_test.go b/pkg/registry/apis/secret/service/consolidation_test.go index 638ced19a81..381cf956436 100644 --- a/pkg/registry/apis/secret/service/consolidation_test.go +++ b/pkg/registry/apis/secret/service/consolidation_test.go @@ -97,7 +97,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) originalDecryptedValues = append(originalDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotNil(t, encryptedValue) originalEncryptedData = append(originalEncryptedData, encryptedValue.EncryptedData) @@ -115,7 +115,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, originalDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, originalEncryptedData[i], encryptedValue.EncryptedData) } @@ -174,7 +174,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) initialDecryptedValues = append(initialDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) initialEncryptedData = append(initialEncryptedData, encryptedValue.EncryptedData) } @@ -223,7 +223,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) newSecretDecryptedValues = append(newSecretDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) newSecretEncryptedData = append(newSecretEncryptedData, encryptedValue.EncryptedData) } @@ -252,7 +252,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, initialDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, initialEncryptedData[i], encryptedValue.EncryptedData) } @@ -275,7 +275,7 @@ func TestConsolidation(t *testing.T) { // Verify that the encrypted data has changed from what it was when first created // (indicating it was re-encrypted during consolidation) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, newSecretEncryptedData[i], encryptedValue.EncryptedData) } diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 2ad69b759db..cc4e6c80c72 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -146,7 +146,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv } logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) - secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, newSecureValue.Name, currentVersion.Status.Version) + secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version) if err != nil { return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) } @@ -191,7 +191,7 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b // TODO: can we stop using external id? // TODO: store uses only the namespace and returns and id. It could be a kv instead. // TODO: check that the encrypted store works with multiple versions - externalID, err := keeper.Store(ctx, keeperCfg, createdSv.Namespace, createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) if err != nil { return nil, fmt.Errorf("storing secure value in keeper: %w", err) } diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 50190dbb3ff..2d1c94c2ea3 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -126,7 +126,14 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { globalEncryptedValueStorage, err := encryptionstorage.ProvideGlobalEncryptedValueStorage(database, tracer) require.NoError(t, err) - sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, nil) + // Initialize a noop migration executor for the sql keeper so it doesn't interfere with initialization + noopMigrationExecutor := &NoopMigrationExecutor{} + sqlKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, noopMigrationExecutor, nil) + require.NoError(t, err) + + // Initialize a real migration executor for test + realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage) + require.NoError(t, err) var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) @@ -158,39 +165,41 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { keeperService) return Sut{ - SecureValueService: secureValueService, - SecureValueMetadataStorage: secureValueMetadataStorage, - DecryptStorage: decryptStorage, - DecryptService: decryptService, - EncryptedValueStorage: encryptedValueStorage, - GlobalEncryptedValueStorage: globalEncryptedValueStorage, - SQLKeeper: sqlKeeper, - Database: database, - AccessClient: accessClient, - ConsolidationService: consolidationService, - EncryptionManager: encryptionManager, - GlobalDataKeyStore: globalDataKeyStore, - GarbageCollectionWorker: garbageCollectionWorker, - Clock: clock, - KeeperService: keeperService, - KeeperMetadataStorage: keeperMetadataStorage, + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + EncryptedValueStorage: encryptedValueStorage, + GlobalEncryptedValueStorage: globalEncryptedValueStorage, + EncryptedValueMigrationExecutor: realMigrationExecutor, + SQLKeeper: sqlKeeper, + Database: database, + AccessClient: accessClient, + ConsolidationService: consolidationService, + EncryptionManager: encryptionManager, + GlobalDataKeyStore: globalDataKeyStore, + GarbageCollectionWorker: garbageCollectionWorker, + Clock: clock, + KeeperService: keeperService, + KeeperMetadataStorage: keeperMetadataStorage, } } type Sut struct { - SecureValueService contracts.SecureValueService - SecureValueMetadataStorage contracts.SecureValueMetadataStorage - DecryptStorage contracts.DecryptStorage - DecryptService decryptcontracts.DecryptService - EncryptedValueStorage contracts.EncryptedValueStorage - GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage - SQLKeeper *sqlkeeper.SQLKeeper - Database *database.Database - AccessClient types.AccessClient - ConsolidationService contracts.ConsolidationService - EncryptionManager contracts.EncryptionManager - GlobalDataKeyStore contracts.GlobalDataKeyStorage - GarbageCollectionWorker *garbagecollectionworker.Worker + SecureValueService contracts.SecureValueService + SecureValueMetadataStorage contracts.SecureValueMetadataStorage + DecryptStorage contracts.DecryptStorage + DecryptService decryptcontracts.DecryptService + EncryptedValueStorage contracts.EncryptedValueStorage + GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage + EncryptedValueMigrationExecutor contracts.EncryptedValueMigrationExecutor + SQLKeeper *sqlkeeper.SQLKeeper + Database *database.Database + AccessClient types.AccessClient + ConsolidationService contracts.ConsolidationService + EncryptionManager contracts.EncryptionManager + GlobalDataKeyStore contracts.GlobalDataKeyStorage + GarbageCollectionWorker *garbagecollectionworker.Worker // The fake clock passed to implementations to make testing easier Clock *FakeClock KeeperService contracts.KeeperService @@ -366,3 +375,10 @@ func (c *FakeClock) Now() time.Time { func (c *FakeClock) AdvanceBy(duration time.Duration) { c.Current = c.Current.Add(duration) } + +type NoopMigrationExecutor struct { +} + +func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) { + return 0, nil +} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 9ee5cfeaf68..5422ac453ae 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -444,6 +444,7 @@ var wireBasicSet = wire.NewSet( secretencryption.ProvideGlobalDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, secretencryption.ProvideGlobalEncryptedValueStorage, + secretencryption.ProvideEncryptedValueMigrationExecutor, secretsecurevalueservice.ProvideSecureValueService, secretvalidator.ProvideKeeperValidator, secretvalidator.ProvideSecureValueValidator, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index bcbb569df18..7d9a66eeeaa 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -216,7 +216,7 @@ import ( kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/secrets/migrator" + migrator2 "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" manager3 "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" @@ -254,7 +254,7 @@ import ( database4 "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" - migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" @@ -481,7 +481,20 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, dependencyRegisterer) if err != nil { return nil, err } @@ -686,7 +699,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -853,11 +866,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) - if err != nil { - return nil, err - } apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { @@ -1083,7 +1091,20 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, dependencyRegisterer) if err != nil { return nil, err } @@ -1290,7 +1311,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -1457,11 +1478,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) - if err != nil { - return nil, err - } apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { @@ -1531,7 +1547,7 @@ func InitializeForCLI(ctx context.Context, cfg *setting.Cfg) (Runner, error) { if err != nil { return Runner{}, err } - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) configProvider, err := configprovider.ProvideService(cfg) if err != nil { return Runner{}, err @@ -1669,7 +1685,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator2.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/storage/secret/encryption/data/encrypted_value_create.sql b/pkg/storage/secret/encryption/data/encrypted_value_create.sql index 7bce6a19bd4..7aa749e01a3 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_create.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_create.sql @@ -3,6 +3,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Arg .Row.Name }}, {{ .Arg .Row.Version }}, {{ .Arg .Row.EncryptedData }}, + {{ .Arg .Row.DataKeyID }}, {{ .Arg .Row.Created }}, {{ .Arg .Row.Updated }} ); diff --git a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql index d318517346d..2a0446a3a9d 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_read.sql b/pkg/storage/secret/encryption/data/encrypted_value_read.sql index 6a672554efb..3ff20a641f0 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_read.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_read.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_update.sql b/pkg/storage/secret/encryption/data/encrypted_value_update.sql index 08e985297a1..c939c092c1c 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_update.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_update.sql @@ -2,6 +2,7 @@ UPDATE {{ .Ident "secret_encrypted_value" }} SET {{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, + {{ .Ident "data_key_id" }} = {{ .Arg .DataKeyID }}, {{ .Ident "updated" }} = {{ .Arg .Updated }} WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND diff --git a/pkg/storage/secret/encryption/encrypted_value_model.go b/pkg/storage/secret/encryption/encrypted_value_model.go index eb66247c021..3c4ba7be905 100644 --- a/pkg/storage/secret/encryption/encrypted_value_model.go +++ b/pkg/storage/secret/encryption/encrypted_value_model.go @@ -6,6 +6,7 @@ type EncryptedValue struct { Namespace string Name string Version int64 + DataKeyID string EncryptedData []byte Created int64 Updated int64 diff --git a/pkg/storage/secret/encryption/encrypted_value_store.go b/pkg/storage/secret/encryption/encrypted_value_store.go index 3a10d01b957..af48a651868 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store.go +++ b/pkg/storage/secret/encryption/encrypted_value_store.go @@ -1,7 +1,9 @@ package encryption import ( + "bytes" "context" + "encoding/base64" "errors" "fmt" "time" @@ -10,6 +12,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -37,9 +40,9 @@ type encryptedValStorage struct { tracer trace.Tracer } -func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { +func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -56,10 +59,11 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string createdTime := time.Now().Unix() encryptedValue := &EncryptedValue{ - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Created: createdTime, Updated: createdTime, } @@ -88,18 +92,21 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error { +func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,10 +114,11 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string req := updateEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Updated: time.Now().Unix(), } @@ -133,9 +141,9 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string return nil } -func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) { +func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -143,7 +151,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v req := &readEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -163,7 +171,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } var encryptedValue EncryptedValue - err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) + err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated) if err != nil { return nil, fmt.Errorf("failed to scan encrypted value row: %w", err) } @@ -172,18 +180,21 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error { +func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -191,7 +202,7 @@ func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string req := deleteEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -264,6 +275,7 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. &row.Name, &row.Version, &row.EncryptedData, + &row.DataKeyID, &row.Created, &row.Updated, ) @@ -272,12 +284,15 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. } encryptedValues = append(encryptedValues, &contracts.EncryptedValue{ - Namespace: row.Namespace, - Name: row.Name, - Version: row.Version, - EncryptedData: row.EncryptedData, - Created: row.Created, - Updated: row.Updated, + Namespace: row.Namespace, + Name: row.Name, + Version: row.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: row.DataKeyID, + EncryptedData: row.EncryptedData, + }, + Created: row.Created, + Updated: row.Updated, }) } if err := rows.Err(); err != nil { @@ -329,3 +344,77 @@ func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int return count, nil } + +type encryptedValMigrationExecutor struct { + db contracts.Database + dialect sqltemplate.Dialect + tracer trace.Tracer + encryptedValueStore contracts.EncryptedValueStorage + globalStore contracts.GlobalEncryptedValueStorage +} + +func ProvideEncryptedValueMigrationExecutor( + db contracts.Database, + tracer trace.Tracer, + encryptedValueStore contracts.EncryptedValueStorage, + globalStore contracts.GlobalEncryptedValueStorage, +) (contracts.EncryptedValueMigrationExecutor, error) { + return &encryptedValMigrationExecutor{ + db: db, + dialect: sqltemplate.DialectForDriver(db.DriverName()), + tracer: tracer, + encryptedValueStore: encryptedValueStore, + globalStore: globalStore, + }, nil +} + +func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) { + ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute") + defer span.End() + + // 1. Retrieve all encrypted values + encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil) + if err != nil { + return 0, fmt.Errorf("listing all encrypted values: %w", err) + } + + // This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail + rowsAffected := 0 + for _, encryptedValue := range encryptedValues { + // 2. If the value already has the data key id broken out, skip it + if encryptedValue.DataKeyID != "" { + continue + } + + // 3. Split the data key id and the encrypted data out from the encoded payload + payload := encryptedValue.EncryptedData + const keyIdDelimiter = '#' + payload = payload[1:] + endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) + if endOfKey == -1 { + return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + b64Key := payload[:endOfKey] + encryptedData := payload[endOfKey+1:] + if len(encryptedData) == 0 { + return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key))) + _, err := base64.RawStdEncoding.Decode(keyId, b64Key) + if err != nil { + return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + + // 4. Update the encrypted value with the data key id and the encrypted data + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{ + DataKeyID: string(keyId), + EncryptedData: encryptedData, + }) + if err != nil { + return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + rowsAffected++ + } + + return rowsAffected, nil +} diff --git a/pkg/storage/secret/encryption/encrypted_value_store_test.go b/pkg/storage/secret/encryption/encrypted_value_store_test.go index 620a034f22c..2f84e1f8575 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store_test.go +++ b/pkg/storage/secret/encryption/encrypted_value_store_test.go @@ -2,15 +2,25 @@ package encryption_test import ( "bytes" + "context" + "encoding/base64" "errors" + "fmt" "slices" "testing" + "text/template" "time" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + cipherService "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" "pgregory.net/rapid" ) @@ -21,7 +31,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) require.NotEmpty(t, createdEV.Namespace) require.NotEmpty(t, createdEV.Name) @@ -36,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) @@ -47,6 +63,7 @@ func TestEncryptedValueStoreImpl(t *testing.T) { require.Equal(t, createdEV.Created, obtainedEV.Created) require.Equal(t, createdEV.Updated, obtainedEV.Updated) require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) + require.Equal(t, createdEV.DataKeyID, obtainedEV.DataKeyID) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) }) @@ -54,7 +71,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version) @@ -78,16 +98,23 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated")) + err = sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id-updated", + EncryptedData: []byte("test-data-updated"), + }) require.NoError(t, err) - updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) + require.Equal(t, "test-data-key-id-updated", updatedEV.DataKeyID) require.Equal(t, createdEV.Created, updatedEV.Created) require.Equal(t, createdEV.Namespace, updatedEV.Namespace) }) @@ -96,7 +123,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data")) + err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.Error(t, err) }) @@ -104,16 +134,19 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("ttttest-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + _, err = sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + err = sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.Error(t, err) require.Nil(t, obtainedEV) }) @@ -130,10 +163,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) // List all encrypted values, without pagination @@ -180,10 +219,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) count, err := sut.GlobalEncryptedValueStorage.CountAll(t.Context(), nil) @@ -198,6 +243,281 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) } +func TestEncryptedValueMigration(t *testing.T) { + t.Parallel() + + t.Run("golden path - successful migration of legacy format", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + tracer := noop.NewTracerProvider().Tracer("test") + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := cipherService.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + testCases := []struct { + namespace string + name string + version int64 + plaintext string + dataKeyId string + }{ + { + namespace: "test-namespace-1", + name: "test-name-1", + version: 1, + plaintext: "test-plaintext-1", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-1", + name: "test-name-2", + version: 1, + plaintext: "test-plaintext-2", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-2", + name: "test-name-3", + version: 1, + plaintext: "test-plaintext-3", + dataKeyId: "test-data-key-id-2", + }, + } + + // Seed with data in the legacy format + for _, tc := range testCases { + err := createLegacyEncryptedData(t, sut, enc, tc.namespace, tc.name, tc.version, tc.plaintext, tc.dataKeyId) + require.NoError(t, err) + } + + // Run the migration and blindy trust it + rowsAffected, err := sut.EncryptedValueMigrationExecutor.Execute(t.Context()) + require.NoError(t, err) + require.Equal(t, len(testCases), rowsAffected) + + // Now validate that the data is in the new format + encryptedValues, err := sut.GlobalEncryptedValueStorage.ListAll(t.Context(), contracts.ListOpts{}, nil) + require.NoError(t, err) + require.Len(t, encryptedValues, 3) + + for _, tc := range testCases { + ev, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(tc.namespace), tc.name, tc.version) + require.NoError(t, err) + + // Decrypt the encrypted data and check for equality + decrypted, err := enc.Decrypt(t.Context(), ev.EncryptedData, tc.dataKeyId) + require.NoError(t, err) + require.Equal(t, tc.dataKeyId, ev.DataKeyID) + require.Equal(t, tc.plaintext, string(decrypted)) + } + }) + + t.Run("error conditions - handles corrupt data gracefully", func(t *testing.T) { + t.Parallel() + + tracer := noop.NewTracerProvider().Tracer("test") + sut := testutils.Setup(t) + + t.Run("global store list error", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + listAllError: errors.New("database connection failed"), + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "listing all encrypted values") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - missing key delimiter", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("corrupt-data-without-delimiter"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "could not find valid key id in encrypted payload") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - empty encrypted data", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#"), // Valid key but no encrypted data after delimiter + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "encrypted data is empty") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - invalid base64 key", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#invalid-base64!@#$%^&*()#somedata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "decoding key id") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("update failure", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "nonexistent-ns", + Name: "nonexistent-name", + Version: 999, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#someencrypteddata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "updating encrypted value") + require.Equal(t, 0, rowsAffected) + }) + }) +} + +// Helper function that bypasses interfaces and creates data in the legacy format directly in the database. +// The format is "#{encoded_key_id}#{encrypted_data}". +func createLegacyEncryptedData(t *testing.T, sut testutils.Sut, enc cipher.Cipher, namespace, name string, version int64, plaintext string, dataKeyId string) error { + t.Helper() + + encryptedData, err := enc.Encrypt(t.Context(), []byte(plaintext), dataKeyId) + require.NoError(t, err) + + // Encode using the legacy format + const keyIdDelimiter = '#' + prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len(dataKeyId))+2) + base64.RawStdEncoding.Encode(prefix[1:], []byte(dataKeyId)) + prefix[0] = keyIdDelimiter + prefix[len(prefix)-1] = keyIdDelimiter + + blob := make([]byte, len(prefix)+len(encryptedData)) + copy(blob, prefix) + copy(blob[len(prefix):], encryptedData) + + createdTime := time.Now().Unix() + + encryptedValue := &encryption.EncryptedValue{ + Namespace: namespace, + Name: name, + Version: version, + EncryptedData: blob, + DataKeyID: "", + Created: createdTime, + Updated: createdTime, + } + + req := struct { + sqltemplate.SQLTemplate + Row *encryption.EncryptedValue + }{ + SQLTemplate: sqltemplate.New(sqltemplate.DialectForDriver(sut.Database.DriverName())), + Row: encryptedValue, + } + tmpl, err := template.ParseFiles("data/encrypted_value_create.sql") + if err != nil { + return fmt.Errorf("parsing template: %w", err) + } + + query, err := sqltemplate.Execute(tmpl, req) + if err != nil { + return fmt.Errorf("executing template: %w", err) + } + + res, err := sut.Database.ExecContext(t.Context(), query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("inserting row: %w", err) + } + + if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } else if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d", rowsAffected) + } + + return nil +} + func TestStateMachine(t *testing.T) { t.Parallel() @@ -212,10 +532,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - _, modelErr := m.create(ns, name, version, []byte(plaintext)) - _, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext)) + _, modelErr := m.create(ns, name, version, []byte(plaintext), dataKeyId) + _, err := sut.EncryptedValueStorage.Create(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -225,10 +549,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - modelErr := m.update(ns, name, version, []byte(plaintext)) - err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext)) + modelErr := m.update(ns, name, version, []byte(plaintext), dataKeyId) + err := sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -240,7 +568,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelValue, modelErr := m.get(ns, name, version) - value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version) + value, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -258,7 +586,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelErr := m.delete(ns, name, version) - err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version) + err := sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -290,18 +618,19 @@ type entry struct { name string version int64 encryptedData []byte + dataKeyId string } func newModel() *model { return &model{} } -func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) { +func (m *model) create(namespace, name string, version int64, encryptedData []byte, dataKeyId string) (*contracts.EncryptedValue, error) { v, err := m.get(namespace, name, version) if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) { return nil, err } - // The entry being creted already exists + // The entry being created already exists if v != nil { return nil, encryption.ErrEncryptedValueAlreadyExists } @@ -311,20 +640,25 @@ func (m *model) create(namespace, name string, version int64, encryptedData []by name: name, version: version, encryptedData: encryptedData, + dataKeyId: dataKeyId, }) return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: encryptedData, + }, + Created: 1, + Updated: 1, }, nil } -func (m *model) update(namespace, name string, version int64, encryptedData []byte) error { +func (m *model) update(namespace, name string, version int64, encryptedData []byte, dataKeyId string) error { for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { v.encryptedData = encryptedData + v.dataKeyId = dataKeyId return nil } } @@ -336,12 +670,15 @@ func (m *model) get(namespace, name string, version int64) (*contracts.Encrypted for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: v.encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: v.dataKeyId, + EncryptedData: v.encryptedData, + }, + Created: 1, + Updated: 1, }, nil } } @@ -354,3 +691,25 @@ func (m *model) delete(namespace, name string, version int64) error { }) return nil } + +// mockGlobalEncryptedValueStorage is a mock implementation of contracts.GlobalEncryptedValueStorage +// used for testing error conditions in the migration executor +type mockGlobalEncryptedValueStorage struct { + encryptedValues []*contracts.EncryptedValue + listAllError error + countAllError error +} + +func (m *mockGlobalEncryptedValueStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) { + if m.listAllError != nil { + return nil, m.listAllError + } + return m.encryptedValues, nil +} + +func (m *mockGlobalEncryptedValueStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) { + if m.countAllError != nil { + return 0, m.countAllError + } + return int64(len(m.encryptedValues)), nil +} diff --git a/pkg/storage/secret/encryption/query.go b/pkg/storage/secret/encryption/query.go index 47ea83ce0cd..21e6081a7c7 100644 --- a/pkg/storage/secret/encryption/query.go +++ b/pkg/storage/secret/encryption/query.go @@ -74,6 +74,7 @@ type updateEncryptedValue struct { Name string Version int64 EncryptedData []byte + DataKeyID string Updated int64 } diff --git a/pkg/storage/secret/encryption/query_test.go b/pkg/storage/secret/encryption/query_test.go index a93ff0b77e6..4cbb830f0af 100644 --- a/pkg/storage/secret/encryption/query_test.go +++ b/pkg/storage/secret/encryption/query_test.go @@ -24,6 +24,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Created: 1234, Updated: 5678, }, @@ -50,6 +51,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Updated: 5679, }, }, diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql index 7a886b1c6ee..952c04730a8 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO `secret_encrypted_value` ( `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO `secret_encrypted_value` ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql index 74b699d45f1..e41d58588d4 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql index b34496aaf93..c51a40a9a5a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql index 9c33fd6bdbf..de82acacbbd 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql index d7066395a78..f21f7122646 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql index bb9faddb8ff..bca020a26d3 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql index cf3d6897a64..b454cb5165a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE `secret_encrypted_value` SET `encrypted_data` = '[115 101 99 114 101 116]', + `data_key_id` = 'test-data-key-id', `updated` = 5679 WHERE `namespace` = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index d148e365925..bd8ad2f73b9 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -134,7 +134,7 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed) } - exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), name, sv.Status.Version) + exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version) if err != nil { return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed) } diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index 34b56a905df..b08fb766e96 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -200,4 +200,15 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { mg.AddMigration("add lease_created index to "+TableNameSecureValue, migrator.NewAddIndexMigration(secureValueTable, &migrator.Index{ Cols: []string{"lease_created"}, })) + + mg.AddMigration("add data_key_id column to "+TableNameEncryptedValue, migrator.NewAddColumnMigration(encryptedValueTable, &migrator.Column{ + Name: "data_key_id", + Type: migrator.DB_NVarchar, + Length: 100, + Nullable: false, + Default: "''", + })) + mg.AddMigration("add data_key_id index to "+TableNameEncryptedValue, migrator.NewAddIndexMigration(encryptedValueTable, &migrator.Index{ + Cols: []string{"data_key_id"}, + })) } From 277fc492bd57f0a13e3d00f4ba4d72c3a5146e14 Mon Sep 17 00:00:00 2001 From: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:43:18 -0500 Subject: [PATCH 14/19] alerting docs: new AI template helper (#111856) * alerting docs: new AI tools docs for alert history and template ai tools * Update docs/sources/alerting/alerting-rules/templates/_index.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * edit word * Update view-alert-state-history.md * Update docs/sources/alerting/alerting-rules/templates/_index.md Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> * prettier --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> --- .../alerting-rules/templates/_index.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/sources/alerting/alerting-rules/templates/_index.md b/docs/sources/alerting/alerting-rules/templates/_index.md index a2afde85de7..f66781f01e5 100644 --- a/docs/sources/alerting/alerting-rules/templates/_index.md +++ b/docs/sources/alerting/alerting-rules/templates/_index.md @@ -220,6 +220,24 @@ To preview label values, select `Use notification policy`, and then click on `Pr {{< figure src="/media/docs/alerting/alert-instance-routing-preview.png" max-width="1200px" alt="Routing preview displays label values" >}} +## Grafana Cloud AI-generated templates + +Grafana Cloud users can use built-in AI tool to generate templates in the appropriate [alerting template language](/docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/language/) for you. + +To use AI to create your template, follow these steps: + +1. Go to **Alerting -> Contact points**. + +1. Click the Notification Templates tab then, click the **+ Add notification template group** button. + +1. Name your template. + +1. In the Template group section, click the **Generate with AI** button. + +1. Supply the AI tool with a prompt or select from one of the example prompts and edit that if necessary. + +1. Click **Save**. + ## More information For further details on how to template alert rules, refer to: From 45fe8c6612d41121fe43cd495fc56febb53f9813 Mon Sep 17 00:00:00 2001 From: Costa Alexoglou Date: Fri, 3 Oct 2025 23:18:04 +0200 Subject: [PATCH 15/19] fix: version migration (#112022) * fix: version migration * fix: cyclomatic --- .../pkg/migration/schemaversion/v16.go | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/apps/dashboard/pkg/migration/schemaversion/v16.go b/apps/dashboard/pkg/migration/schemaversion/v16.go index f360fe246ad..d8fb357c3ef 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16.go @@ -128,22 +128,18 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { continue } - // Set default span (line 1063 in TS) - span := GetFloatValue(panel, "span", defaultPanelSpan) + // Check if panel already has gridPos but no valid span + // If span is missing or zero, and gridPos exists, preserve gridPos dimensions + var panelWidth, panelHeight int + span := GetFloatValue(panel, "span", 0) + existingGridPos, hasGridPos := panel["gridPos"].(map[string]interface{}) - // Handle minSpan conversion (lines 1064-1066 in TS) - if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan { - if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 { - panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat)) - } - } - - panelWidth := int(math.Floor(span * widthFactor)) - panelHeight := rowGridHeight - if panelHeightValue, hasHeight := panel["height"]; hasHeight { - if h, ok := ConvertToFloat(panelHeightValue); ok { - panelHeight = getGridHeight(h) - } + if hasGridPos && span == 0 { + // Panel already has gridPos but no valid span - preserve its dimensions + panelWidth = GetIntValue(existingGridPos, "w", int(defaultPanelSpan*widthFactor)) + panelHeight = GetIntValue(existingGridPos, "h", rowGridHeight) + } else { + panelWidth, panelHeight = calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight) } panelPos := rowArea.getPanelPosition(panelHeight, panelWidth) @@ -315,3 +311,27 @@ func getGridHeight(height float64) int { } return int(math.Ceil(height / panelHeightStep)) } + +func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{}, widthFactor float64, defaultHeight int) (int, int) { + // Set default span if still 0 + if span == 0 { + span = defaultPanelSpan + } + + if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan { + if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 { + panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat)) + } + } + + panelWidth := int(math.Floor(span * widthFactor)) + panelHeight := defaultHeight + + if panelHeightValue, hasHeight := panel["height"]; hasHeight { + if h, ok := ConvertToFloat(panelHeightValue); ok { + panelHeight = getGridHeight(h) + } + } + + return panelWidth, panelHeight +} From 557cc2941bc276a01c9118d315b1afc7be7fe8ef Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 00:37:27 +0000 Subject: [PATCH 16/19] I18n: Download translations from Crowdin (#112027) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 8 +++++++- public/locales/de-DE/grafana.json | 8 +++++++- public/locales/es-ES/grafana.json | 8 +++++++- public/locales/fr-FR/grafana.json | 8 +++++++- public/locales/hu-HU/grafana.json | 8 +++++++- public/locales/id-ID/grafana.json | 8 +++++++- public/locales/it-IT/grafana.json | 8 +++++++- public/locales/ja-JP/grafana.json | 8 +++++++- public/locales/ko-KR/grafana.json | 8 +++++++- public/locales/nl-NL/grafana.json | 8 +++++++- public/locales/pl-PL/grafana.json | 8 +++++++- public/locales/pt-BR/grafana.json | 8 +++++++- public/locales/pt-PT/grafana.json | 8 +++++++- public/locales/ru-RU/grafana.json | 8 +++++++- public/locales/sv-SE/grafana.json | 8 +++++++- public/locales/tr-TR/grafana.json | 8 +++++++- public/locales/zh-Hans/grafana.json | 8 +++++++- public/locales/zh-Hant/grafana.json | 8 +++++++- 18 files changed, 126 insertions(+), 18 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 2f3a92ee6ea..e2048107f20 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -3264,7 +3264,7 @@ "define-allowed-teams-ids-label": "Definovat povolená ID týmů", "display-name-description": "Zobrazí se na přihlašovací stránce jako „Přihlásit se přes…“. Užitečné, pokud používáte více než jednoho poskytovatele identity nebo protokoly SSO.", "display-name-label": "Zobrazované jméno", - "domain-hint-description": "Parametr pro označení oblasti uživatele v instanci Azure AD / Entra ID a zjednodušení procesu přihlášení.", + "domain-hint-description": "", "domain-hint-label": "Nápověda k doméně", "domain-hint-valid-domain": "Toto pole musí být platná doména.", "email-attribute-name-description": "Název klíče, který se použije pro vyhledání e-mailu uživatele v mapě atributů ID tokenu OAuth2.", @@ -8743,6 +8743,9 @@ "tooltip-hide": "Skrýt heslo", "tooltip-show": "Zobrazit heslo" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Rozbalit řádek", "collapse": "Sbalit řádek", @@ -8771,6 +8774,9 @@ "series-color-picker-popover": { "y-axis-usage": "Použít pravou osu y" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Načítání" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index eb8f0d1d395..1bc8591394c 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Erlaubte Team-IDs festlegen", "display-name-description": "Wird auf der Anmeldeseite als „Anmelden mit …“ angezeigt. Hilfreich, wenn Sie mehr als einen Identitätsanbieter oder SSO-Protokolle nutzen.", "display-name-label": "Anzeigename", - "domain-hint-description": "Parameter zur Angabe des Bereichs des Nutzers im Azure AD/Entra ID-Tenant und zur Optimierung des Anmeldeprozesses.", + "domain-hint-description": "", "domain-hint-label": "Domain-Hinweis", "domain-hint-valid-domain": "Dieses Feld muss eine gültige Domain sein.", "email-attribute-name-description": "Name des Keys, der für die Nutzer-E-Mail-Suche in der Attributzuordnung des OAuth2-ID-Tokens verwendet wird.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Passwort verbergen", "tooltip-show": "Passwort anzeigen" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Zeile ausklappen", "collapse": "Zeile einklappen", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Rechte y-Achse verwenden" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Wird geladen" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 099476d81f0..4c49f7e259e 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Definir ID de equipos permitidos", "display-name-description": "Se mostrará en la página de inicio de sesión como «Iniciar sesión con...». Resulta útil si utiliza más de un proveedor de identidad o protocolos SSO.", "display-name-label": "Nombre para mostrar", - "domain-hint-description": "Parámetro para indicar el ámbito del usuario en el inquilino de Azure AD/Entra ID y agilizar el proceso de inicio de sesión.", + "domain-hint-description": "", "domain-hint-label": "Sugerencia de dominio", "domain-hint-valid-domain": "Este campo debe ser un dominio válido.", "email-attribute-name-description": "Nombre de la clave que se utilizará para la búsqueda de correo electrónico del usuario dentro del mapa de atributos del token de identificación OAuth2.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Ocultar contraseña", "tooltip-show": "Mostrar contraseña" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Expandir fila", "collapse": "Contraer fila", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Usar eje y derecho" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Cargando" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 81e72c3a19b..b88e35f8c48 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Définir les ID des équipes autorisées", "display-name-description": "Sera affiché sur la page de connexion comme « Se connecter avec... ». Utile si vous utilisez plusieurs fournisseurs d’identité ou protocoles SSO.", "display-name-label": "Pseudo", - "domain-hint-description": "Paramètre permettant d’indiquer le domaine de l’utilisateur dans le locataire Azure AD/Entra ID pour simplifier le processus de connexion.", + "domain-hint-description": "", "domain-hint-label": "Indice de domaine", "domain-hint-valid-domain": "Ce champ doit contenir un domaine valide.", "email-attribute-name-description": "Nom de la clé à utiliser pour la recherche d’e-mail de l’utilisateur dans la carte d’attributs du jeton d’identification OAuth2.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Masquer le mot de passe", "tooltip-show": "Afficher le mot de passe" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Développer la ligne", "collapse": "Réduire la ligne", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Utiliser l'axe y droit" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Chargement en cours" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index d3413be6f75..82ca4a86f87 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Engedélyezett csapatazonosítók meghatározása", "display-name-description": "A bejelentkezési oldalon „Bejelentkezés ezzel” felirattal jelenik meg. Hasznos, ha egynél több identitásszolgáltatót vagy SSO-protokollt használ.", "display-name-label": "Megjelenített név", - "domain-hint-description": "Paraméter, amely jelzi a felhasználó tartományát az Azure AD/Entra ID bérlőben, és egyszerűsíti a bejelentkezést.", + "domain-hint-description": "", "domain-hint-label": "Tipp a domainhez", "domain-hint-valid-domain": "Ebben a mezőben csak érvényes domain szerepelhet.", "email-attribute-name-description": "Az OAuth2-azonosítótoken attribútumtérképén belül a felhasználói e-mail-kereséshez használandó kulcs neve.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Jelszó elrejtése", "tooltip-show": "Jelszó megjelenítése" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Sor kibontása", "collapse": "Sor összecsukása", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Jobb y tengely használata" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Betöltés" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 4e1f1914d6c..004a4b57582 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -3228,7 +3228,7 @@ "define-allowed-teams-ids-label": "Tentukan ID tim yang diizinkan", "display-name-description": "Akan ditampilkan di halaman login sebagai \"Masuk dengan ...\". Berguna jika Anda menggunakan lebih dari satu penyedia identitas atau protokol SSO.", "display-name-label": "Nama tampilan", - "domain-hint-description": "Parameter untuk menunjukkan domain pengguna di penyewa Azure AD/Entra ID dan menyederhanakan proses masuk.", + "domain-hint-description": "", "domain-hint-label": "Petunjuk domain", "domain-hint-valid-domain": "Bidang ini harus berupa domain yang valid.", "email-attribute-name-description": "Nama kunci yang akan digunakan untuk pencarian email pengguna dalam peta atribut token ID OAuth2.", @@ -8674,6 +8674,9 @@ "tooltip-hide": "Sembunyikan kata sandi", "tooltip-show": "Tampilkan kata sandi" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Perbesar baris", "collapse": "Ciutkan baris", @@ -8702,6 +8705,9 @@ "series-color-picker-popover": { "y-axis-usage": "Gunakan sumbu y kanan" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Memuat" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 749e12ad04e..167081837d0 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Definisci gli ID dei team consentiti", "display-name-description": "Verrà visualizzato nella pagina di accesso come \"Accedi con...\". Utile se utilizzi più di un provider di identità o protocolli SSO.", "display-name-label": "Visualizza nome", - "domain-hint-description": "Parametro per indicare il dominio dell'utente nel tenant Azure AD/Entra ID e semplificare il processo di accesso.", + "domain-hint-description": "", "domain-hint-label": "Suggerimento dominio", "domain-hint-valid-domain": "Questo campo deve essere un dominio valido.", "email-attribute-name-description": "Nome della chiave da utilizzare per la ricerca dell'e-mail dell'utente all'interno della mappa degli attributi del token ID di OAuth2.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Nascondi password", "tooltip-show": "Mostra password" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Espandi riga", "collapse": "Riduci riga", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Usa l'asse y destro" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Caricamento" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 59de942e78e..b5a187415c6 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -3228,7 +3228,7 @@ "define-allowed-teams-ids-label": "許可されたチームIDを定義する", "display-name-description": "ログインページに「...でサインイン」と表示されます。複数のIDプロバイダーまたはSSOプロトコルを使用する場合に役立ちます。", "display-name-label": "表示名", - "domain-hint-description": "Azure AD/Entra IDテナントでユーザーの領域を示し、ログインプロセスを合理化するためのパラメーター。", + "domain-hint-description": "", "domain-hint-label": "ドメインヒント", "domain-hint-valid-domain": "このフィールドには有効なドメインを入力する必要があります。", "email-attribute-name-description": "OAuth2 IDトークンの属性マップ内でユーザーメール検索に使用するキーの名前。", @@ -8674,6 +8674,9 @@ "tooltip-hide": "パスワードを非表示", "tooltip-show": "パスワードを表示" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "行を展開する", "collapse": "行を折りたたむ", @@ -8702,6 +8705,9 @@ "series-color-picker-popover": { "y-axis-usage": "右y軸を使用" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "読込中" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index c93e2bb92a4..f50b3cf61bf 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -3228,7 +3228,7 @@ "define-allowed-teams-ids-label": "허용된 팀 ID 정의", "display-name-description": "로그인 페이지에 '다음으로 로그인'으로 표시됩니다. 두 개 이상의 ID 제공자 또는 SSO 프로토콜을 사용하는 경우 유용합니다.", "display-name-label": "표시 이름", - "domain-hint-description": "Azure AD/Entra ID 테넌트에서 사용자의 영역을 나타내고 로그인 프로세스를 간소화하는 매개변수입니다.", + "domain-hint-description": "", "domain-hint-label": "도메인 힌트", "domain-hint-valid-domain": "이 필드는 유효한 도메인이어야 합니다.", "email-attribute-name-description": "OAuth2 ID 토큰의 속성 맵 내에서 사용자 이메일 조회에 사용할 키의 이름입니다.", @@ -8674,6 +8674,9 @@ "tooltip-hide": "비밀번호 숨기기", "tooltip-show": "비밀번호" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "행 펼치기", "collapse": "행 접기", @@ -8702,6 +8705,9 @@ "series-color-picker-popover": { "y-axis-usage": "오른쪽 y축 사용" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "로딩 중" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index deb2897c99c..e88d4cc4ed9 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Toegestane team-id's definiëren", "display-name-description": "Wordt weergegeven op de inlogpagina als 'Aanmelden met ...'. Handig als je meer dan één identiteitsprovider of SSO-protocol gebruikt.", "display-name-label": "Weergavenaam", - "domain-hint-description": "Parameter om het domein van de gebruiker in de Azure AD/Entra ID-tenant aan te geven en het inlogproces te stroomlijnen.", + "domain-hint-description": "", "domain-hint-label": "Domeinhint", "domain-hint-valid-domain": "Dit veld moet een geldig domein zijn.", "email-attribute-name-description": "Naam van de sleutel die moet worden gebruikt voor het opzoeken van gebruikers-e-mails in de attribuutkaart van het OAuth2 ID-token.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Wachtwoord verbergen", "tooltip-show": "Wachtwoord weergeven" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Rij uitvouwen", "collapse": "Rij samenvouwen", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Rechter y-as gebruiken" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Bezig met laden" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 3aa46a8c694..6e98e44485a 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -3264,7 +3264,7 @@ "define-allowed-teams-ids-label": "Zdefiniuj dozwolone identyfikatory zespołów", "display-name-description": "Nazwa będzie się wyświetlać na stronie logowania jako część tekstu „Zaloguj się za pomocą...”. To przydatne, jeśli korzystasz z więcej niż jednego dostawcy tożsamości lub protokołów SSO.", "display-name-label": "Nazwa wyświetlana", - "domain-hint-description": "Parametr wskazujący dziedzinę użytkownika w dzierżawie Azure AD/Entra ID i usprawniający proces logowania.", + "domain-hint-description": "", "domain-hint-label": "Wskazówka dotycząca domeny", "domain-hint-valid-domain": "To pole musi zawierać prawidłową domenę.", "email-attribute-name-description": "Nazwa klucza używanego do wyszukiwania adresu e-mail użytkownika w mapie atrybutów tokena OAuth2 ID.", @@ -8743,6 +8743,9 @@ "tooltip-hide": "Ukryj hasło", "tooltip-show": "Pokaż hasło" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Rozwiń wiersz", "collapse": "Zwiń wiersz", @@ -8771,6 +8774,9 @@ "series-color-picker-popover": { "y-axis-usage": "Użyj prawej półosi OY" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Ładowanie" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 7c0d99cec65..b3d1d198556 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Definir IDs das equipes permitidas", "display-name-description": "Será exibido na página de login como \"Entrar com...\". É uma opção conveniente caso você use mais de um provedor de identidade ou protocolos SSO.", "display-name-label": "Nome de exibição", - "domain-hint-description": "Parâmetro para indicar o âmbito do usuário no locatário do Azure AD/Entra ID e agilizar o processo de login.", + "domain-hint-description": "", "domain-hint-label": "Dica de domínio", "domain-hint-valid-domain": "Este campo deve ser um domínio válido.", "email-attribute-name-description": "Nome da chave a ser usada para a consulta de e-mail do usuário no mapa de atributos do token de ID OAuth2.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Ocultar senha", "tooltip-show": "Exibir senha" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Expandir linha", "collapse": "Recolher linha", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Usar o eixo y à direita" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Carregando" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 6e87ec75ca5..041324ee396 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Definir as ID das equipas permitidas", "display-name-description": "Será mostrado na página de início de sessão como \"Iniciar sessão com...\". Útil se utilizar mais de um fornecedor de identidade ou protocolos SSO.", "display-name-label": "Nome de exibição", - "domain-hint-description": "Parâmetro para indicar o domínio do utilizador no espaço do Azure AD/Entra ID e agilizar o processo de início de sessão.", + "domain-hint-description": "", "domain-hint-label": "Dica de domínio", "domain-hint-valid-domain": "Este campo deve ser um domínio válido.", "email-attribute-name-description": "Nome da chave a utilizar para a pesquisa de e-mail do utilizador no mapa de atributos do token de ID OAuth2.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Ocultar palavra-passe", "tooltip-show": "Mostrar palavra-passe" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Expandir linha", "collapse": "Recolher linha", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Utilizar o eixo y direito" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "A carregar" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index f95e047aa37..2241b75dede 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -3264,7 +3264,7 @@ "define-allowed-teams-ids-label": "Определить идентификаторы разрешенных команд", "display-name-description": "Будет отображаться на странице входа как «Войти с помощью...». Параметр полезен, если вы используете несколько поставщиков удостоверений или протоколов единого входа.", "display-name-label": "Отображаемое имя", - "domain-hint-description": "Параметр, указывающий область пользователя в клиенте Azure AD/Entra ID и упрощающий процесс входа.", + "domain-hint-description": "", "domain-hint-label": "Подсказка домена", "domain-hint-valid-domain": "В этом поле должен быть указан действительный домен.", "email-attribute-name-description": "Имя ключа, используемого для поиска адреса эл. почты пользователя в карте атрибутов идентификационного токена OAuth2.", @@ -8743,6 +8743,9 @@ "tooltip-hide": "Скрыть пароль", "tooltip-show": "Показать пароль" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Развернуть строку", "collapse": "Свернуть строку", @@ -8771,6 +8774,9 @@ "series-color-picker-popover": { "y-axis-usage": "Использовать правую ось y" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Загрузка" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index e9e31498880..ac34fbbf4e8 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "Definiera tillåtna team-ID", "display-name-description": "Kommer att visas på inloggningssidan som ”Logga in med …” Användbart om du använder fler än en identitetsleverantör eller SSO-protokoll.", "display-name-label": "Visningsnamn", - "domain-hint-description": "Parameter som anger användarens domän i Azure AD/Entra ID-klienten och effektiviserar inloggningsprocessen.", + "domain-hint-description": "", "domain-hint-label": "Domäntips", "domain-hint-valid-domain": "Detta fält måste vara en giltig domän.", "email-attribute-name-description": "Namn på nyckeln som ska användas för användarens e-postsökning inom attributkartan för OAuth2 ID-token.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Dölj lösenord", "tooltip-show": "Visa lösenord" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Expandera rad", "collapse": "Dölj rad", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Använd höger y-axel" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Laddar" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 1e97fd70be4..d0f88d3b83e 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -3240,7 +3240,7 @@ "define-allowed-teams-ids-label": "İzin verilen ekip kimliklerini tanımlayın", "display-name-description": "Giriş sayfasında \"Şununla giriş yap:\" olarak gösterilecektir. Birden fazla kimlik sağlayıcı veya SSO protokolü kullanıyorsanız faydalıdır.", "display-name-label": "Görünen ad", - "domain-hint-description": "Azure AD/Entra ID kiracısında kullanıcının alanını belirtmek ve oturum açma işlemini kolaylaştırmak için parametre.", + "domain-hint-description": "", "domain-hint-label": "Alan adı ipucu", "domain-hint-valid-domain": "Bu alan geçerli bir alan adı olmalıdır.", "email-attribute-name-description": "OAuth2 kimlik belirteci öznitelik eşlemesinde kullanıcı e-postasını aramak için kullanılacak anahtarın adı.", @@ -8697,6 +8697,9 @@ "tooltip-hide": "Parolayı gizle", "tooltip-show": "Parolayı göster" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "Satırı genişlet", "collapse": "Satırı daralt", @@ -8725,6 +8728,9 @@ "series-color-picker-popover": { "y-axis-usage": "Sağ Y eksenini kullan" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "Yükleniyor" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index e03b3dffc2a..42311600f4c 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -3228,7 +3228,7 @@ "define-allowed-teams-ids-label": "定义允许的团队 ID", "display-name-description": "将在登录页面上显示为“使用... 登录”。如果您使用多个身份提供程序或 SSO 协议,这将很有帮助。", "display-name-label": "显示名称", - "domain-hint-description": "用于指示 Azure AD/Entra ID 租户中用户领域并简化登录流程的参数。", + "domain-hint-description": "", "domain-hint-label": "域提示", "domain-hint-valid-domain": "此字段必须是有效的域。", "email-attribute-name-description": "用于在 OAuth2 ID 令牌的属性映射中查找用户电子邮箱的密钥的名称。", @@ -8674,6 +8674,9 @@ "tooltip-hide": "隐藏密码", "tooltip-show": "显示密码" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "展开行", "collapse": "折叠行", @@ -8702,6 +8705,9 @@ "series-color-picker-popover": { "y-axis-usage": "使用右侧 Y 轴" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "正在加载" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 1152d08508e..ed4b4e68ad0 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -3228,7 +3228,7 @@ "define-allowed-teams-ids-label": "定義允許的團隊 ID", "display-name-description": "將在登入頁面上顯示為「使用...登入」。如果您使用多個身分提供者或 SSO 協定,這項功能會很有幫助。", "display-name-label": "顯示名稱", - "domain-hint-description": "用於指示 Azure AD/Entra ID 租用戶中使用者領域的參數,並簡化登入流程。", + "domain-hint-description": "", "domain-hint-label": "網域提示", "domain-hint-valid-domain": "此欄位必須為有效的網域。", "email-attribute-name-description": "設定用來在 OAuth2 ID 權杖的屬性對應中查找使用者電子郵件的金鑰名稱。", @@ -8674,6 +8674,9 @@ "tooltip-hide": "隱藏密碼", "tooltip-show": "顯示密碼" }, + "range-slider": { + "drag-handle-aria-label": "" + }, "row-expander": { "aria-label-expand": "展開列", "collapse": "收闔列", @@ -8702,6 +8705,9 @@ "series-color-picker-popover": { "y-axis-usage": "使用右 y 軸" }, + "slider": { + "drag-handle-aria-label": "" + }, "spinner": { "aria-label": "正在載入" }, From 56b136a8de717eee5d9aea8aadaf35eb36b05930 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 3 Oct 2025 23:30:08 -0600 Subject: [PATCH 17/19] Permission Creator: Remove user type check (#112024) --- pkg/storage/unified/apistore/permissions.go | 5 -- .../unified/apistore/permissions_test.go | 82 ------------------- 2 files changed, 87 deletions(-) diff --git a/pkg/storage/unified/apistore/permissions.go b/pkg/storage/unified/apistore/permissions.go index 95d2099cb86..395ac65f4f2 100644 --- a/pkg/storage/unified/apistore/permissions.go +++ b/pkg/storage/unified/apistore/permissions.go @@ -39,11 +39,6 @@ func afterCreatePermissionCreator(ctx context.Context, return nil, errors.New("missing auth info") } - idtype := auth.GetIdentityType() - if idtype != authtypes.TypeUser && idtype != authtypes.TypeServiceAccount && idtype != authtypes.TypeAccessPolicy { - return nil, fmt.Errorf("only users, service accounts, and access policies may grant permissions using an annotation") - } - return func(ctx context.Context) error { return setter(ctx, key, auth, val) }, nil diff --git a/pkg/storage/unified/apistore/permissions_test.go b/pkg/storage/unified/apistore/permissions_test.go index ff42a4cd189..fab28e6dc37 100644 --- a/pkg/storage/unified/apistore/permissions_test.go +++ b/pkg/storage/unified/apistore/permissions_test.go @@ -9,7 +9,6 @@ import ( authtypes "github.com/grafana/authlib/types" "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -46,85 +45,4 @@ func TestAfterCreatePermissionCreator(t *testing.T) { require.Nil(t, creator) require.Contains(t, err.Error(), "missing auth info") }) - - t.Run("should succeed for user identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeUser, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should succeed for service account identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeServiceAccount, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should succeed for access policy identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeAccessPolicy, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should error for non-user/non-service-account identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeAnonymous, - }) - obj := &v0alpha1.Dashboard{} - - creator, err := afterCreatePermissionCreator(ctx, nil, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.Error(t, err) - require.Nil(t, creator) - require.Contains(t, err.Error(), "only users, service accounts, and access policies may grant permissions") - }) } From dc528cef0de1a337454c2bf814651c806840956f Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Sat, 4 Oct 2025 04:24:13 -0500 Subject: [PATCH 18/19] User API: add missing uid to user look up api (#112017) --- pkg/api/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/user.go b/pkg/api/user.go index 3d84e2cffd6..311f80e49ae 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -117,6 +117,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response } result := user.UserProfileDTO{ ID: usr.ID, + UID: usr.UID, Name: usr.Name, Email: usr.Email, Login: usr.Login, From 99312a7582fb96a6b32b8c90599bd442991991de Mon Sep 17 00:00:00 2001 From: Austin Pond Date: Sat, 4 Oct 2025 05:33:10 -0400 Subject: [PATCH 19/19] Apps: Update grafana-app-sdk to v0.46.0 (#112021) Co-authored-by: Ryan McKinley --- apps/advisor/pkg/app/app.go | 7 +- apps/alerting/alertenrichment/go.mod | 4 +- apps/alerting/alertenrichment/go.sum | 8 +- .../alerting-notifications-manifest.yaml | 1 + apps/alerting/notifications/go.mod | 4 +- apps/alerting/notifications/go.sum | 8 +- .../apis/alertingnotifications_manifest.go | 5 +- apps/alerting/notifications/pkg/app/app.go | 7 +- .../rules/definitions/alerting-manifest.yaml | 1 + apps/alerting/rules/go.mod | 4 +- apps/alerting/rules/go.sum | 8 +- .../rules/pkg/apis/alerting_manifest.go | 5 +- apps/alerting/rules/pkg/app/app.go | 7 +- apps/correlations/go.mod | 4 +- apps/correlations/go.sum | 8 +- .../pkg/apis/correlation_manifest.go | 5 +- apps/correlations/pkg/app/app.go | 7 +- apps/dashboard/go.mod | 4 +- apps/dashboard/go.sum | 8 +- apps/dashboard/pkg/apis/dashboard_manifest.go | 5 +- apps/folder/go.mod | 4 +- apps/folder/go.sum | 8 +- apps/folder/pkg/apis/folder_manifest.go | 5 +- apps/iam/go.mod | 4 +- apps/iam/go.sum | 8 +- apps/iam/pkg/apis/iam_manifest.go | 5 +- apps/iam/pkg/app/app.go | 20 ++-- apps/investigations/go.mod | 4 +- apps/investigations/go.sum | 8 +- .../v0alpha1/investigation_client_gen.go | 4 +- .../v0alpha1/investigationindex_client_gen.go | 4 +- .../pkg/apis/investigations_manifest.go | 28 +++++- .../pkg/app/investigations_app.go | 7 +- apps/playlist/go.mod | 4 +- apps/playlist/go.sum | 8 +- apps/playlist/pkg/apis/playlist_manifest.go | 5 +- apps/playlist/pkg/app/app.go | 6 +- apps/plugins/go.mod | 4 +- apps/plugins/go.sum | 8 +- apps/plugins/pkg/apis/plugins_manifest.go | 5 +- apps/plugins/pkg/app/app.go | 7 +- apps/preferences/go.mod | 4 +- apps/preferences/go.sum | 8 +- .../pkg/apis/preferences_manifest.go | 5 +- apps/provisioning/go.mod | 4 +- apps/provisioning/go.sum | 8 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 4 +- apps/secret/go.sum | 8 +- .../apis/secret/v1beta1/keeper_client_gen.go | 99 +++++++++++++++++++ .../secret/v1beta1/securevalue_client_gen.go | 99 +++++++++++++++++++ apps/secret/pkg/apis/secret_manifest.go | 51 +++++++++- apps/shorturl/go.mod | 4 +- apps/shorturl/go.sum | 8 +- apps/shorturl/kinds/shorturl.cue | 1 + ...=> shorturl_getgoto_response_types_gen.go} | 0 apps/shorturl/pkg/apis/shorturl_manifest.go | 7 +- apps/shorturl/pkg/app/app.go | 7 +- go.mod | 4 +- go.sum | 8 +- go.work.sum | 3 + pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 +- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 +- pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 +- pkg/codegen/go.mod | 2 +- pkg/codegen/go.sum | 4 +- pkg/plugins/codegen/go.mod | 2 +- pkg/plugins/codegen/go.sum | 4 +- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 +- ...cations.alerting.grafana.app-v0alpha1.json | 24 +++++ .../playlist.grafana.app-v0alpha1.json | 6 ++ .../rules.alerting.grafana.app-v0alpha1.json | 12 +++ .../shorturl.grafana.app-v1alpha1.json | 6 ++ .../playlist/v0alpha1/endpoints.gen.ts | 8 +- .../clients/rules/v0alpha1/endpoints.gen.ts | 16 +-- .../shorturl/v1alpha1/endpoints.gen.ts | 8 +- public/app/core/utils/shortLinks.ts | 3 + .../features/playlist/PlaylistForm.test.tsx | 6 ++ .../app/features/playlist/PlaylistSrv.test.ts | 2 + public/app/features/playlist/utils.ts | 2 + 84 files changed, 551 insertions(+), 178 deletions(-) create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go rename apps/shorturl/pkg/apis/shorturl/v1alpha1/{shorturl_getgoto_types_gen.go => shorturl_getgoto_response_types_gen.go} (100%) diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index fae11df0c6c..b572f4b3866 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" @@ -48,8 +49,10 @@ func New(cfg app.Config) (app.App, error) { Name: "advisor", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - log.WithContext(ctx).Error("Informer processing error", "error", err) + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + log.WithContext(ctx).Error("Informer processing error", "error", err) + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index aa670ef15ed..48c93f1aa1e 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,14 +3,14 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b ) require ( - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index 1ff737c6fbd..ead26b4a7b0 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -21,8 +21,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml index 447c5d5f747..aa40d7c74b3 100644 --- a/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml +++ b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml @@ -5,6 +5,7 @@ metadata: spec: appName: alerting-notifications group: notifications.alerting.grafana.app + preferredVersion: v0alpha1 versions: - kinds: - conversion: false diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 4f5e6fc625c..f811c12d2f8 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 @@ -19,7 +19,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 793458ef166..9166be2eae7 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -21,8 +21,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -69,8 +69,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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index 3537a6e3e8d..84850cb31a1 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -34,8 +34,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "alerting-notifications", - Group: "notifications.alerting.grafana.app", + AppName: "alerting-notifications", + Group: "notifications.alerting.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/alerting/notifications/pkg/app/app.go b/apps/alerting/notifications/pkg/app/app.go index b57ed0cd143..65b3882ac59 100644 --- a/apps/alerting/notifications/pkg/app/app.go +++ b/apps/alerting/notifications/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis" @@ -22,8 +23,10 @@ func New(cfg app.Config) (app.App, error) { Name: "alerting.notification", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.DefaultLogger.With("error", err).Error("Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.DefaultLogger.With("error", err).Error("Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/alerting/rules/definitions/alerting-manifest.yaml b/apps/alerting/rules/definitions/alerting-manifest.yaml index 22808e9deba..836bd2c9a94 100644 --- a/apps/alerting/rules/definitions/alerting-manifest.yaml +++ b/apps/alerting/rules/definitions/alerting-manifest.yaml @@ -5,6 +5,7 @@ metadata: spec: appName: alerting group: rules.alerting.grafana.app + preferredVersion: v0alpha1 versions: - kinds: - conversion: false diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 99aab42dd9a..7678f821f3a 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/alerting/rules/pkg/apis/alerting_manifest.go b/apps/alerting/rules/pkg/apis/alerting_manifest.go index abbc407ddf3..ea4f2530948 100644 --- a/apps/alerting/rules/pkg/apis/alerting_manifest.go +++ b/apps/alerting/rules/pkg/apis/alerting_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "alerting", - Group: "rules.alerting.grafana.app", + AppName: "alerting", + Group: "rules.alerting.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/alerting/rules/pkg/app/app.go b/apps/alerting/rules/pkg/app/app.go index 513bcc92d30..ef0ab1883cf 100644 --- a/apps/alerting/rules/pkg/app/app.go +++ b/apps/alerting/rules/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/rules/pkg/apis" @@ -22,8 +23,10 @@ func New(cfg app.Config) (app.App, error) { Name: "alerting.rules", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.DefaultLogger.With("error", err).Error("Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.DefaultLogger.With("error", err).Error("Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 7599bad88d5..2f6538b879d 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/correlations/pkg/apis/correlation_manifest.go b/apps/correlations/pkg/apis/correlation_manifest.go index 4ef118d2216..3a784903323 100644 --- a/apps/correlations/pkg/apis/correlation_manifest.go +++ b/apps/correlations/pkg/apis/correlation_manifest.go @@ -25,8 +25,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "correlation", - Group: "correlations.grafana.app", + AppName: "correlation", + Group: "correlations.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/correlations/pkg/app/app.go b/apps/correlations/pkg/app/app.go index 99a1600bac6..5d62c6c3902 100644 --- a/apps/correlations/pkg/app/app.go +++ b/apps/correlations/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,8 +18,10 @@ func New(cfg app.Config) (app.App, error) { Name: "correlation", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.FromContext(ctx).Error("Informer processing error", "error", err) + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).Error("Informer processing error", "error", err) + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 01ace00792d..adfae61e8d1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,7 +5,7 @@ go 1.24.6 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana-plugin-sdk-go v0.279.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e @@ -31,7 +31,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 13b5f650649..4358d7d815a 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -36,8 +36,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -101,8 +101,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 v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-plugin-sdk-go v0.279.0 h1:/KCrsZkj9pEGwIGovqAz1A8rjI2A2YT+ZpvgfZN0LAA= diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 1e8f261e65b..3ea4e0284b3 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -21,8 +21,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "dashboard", - Group: "dashboard.grafana.app", + AppName: "dashboard", + Group: "dashboard.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 2772136fc3b..6db44655eeb 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -13,7 +13,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 37e7a4c1159..ab3cbcecbb1 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -31,8 +31,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= diff --git a/apps/folder/pkg/apis/folder_manifest.go b/apps/folder/pkg/apis/folder_manifest.go index 37e994e62c6..4fcb76837ed 100644 --- a/apps/folder/pkg/apis/folder_manifest.go +++ b/apps/folder/pkg/apis/folder_manifest.go @@ -18,8 +18,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "folder", - Group: "folder.grafana.app", + AppName: "folder", + Group: "folder.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v1beta1", diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 9bdcbaa3bd9..b196d540f3f 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -22,7 +22,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 @@ -145,7 +145,7 @@ require ( github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 52ebbdf54dd..5cf6786bea4 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -438,8 +438,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -733,8 +733,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index fba76fbd7df..24aba9a1fd8 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -18,8 +18,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "iam", - Group: "iam.grafana.app", + AppName: "iam", + Group: "iam.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index c84128e76b4..05216220d7e 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -36,7 +36,7 @@ func Provider(appCfg app.SpecificConfig) app.Provider { } func generateInformerSupplier(informerConfig InformerConfig, metrics *reconcilers.ReconcilerMetrics) simple.InformerSupplier { - return func(kind resource.Kind, clients resource.ClientGenerator, options operator.ListWatchOptions) (operator.Informer, error) { + return func(kind resource.Kind, clients resource.ClientGenerator, options operator.InformerOptions) (operator.Informer, error) { client, err := clients.ClientFor(kind) if err != nil { return nil, err @@ -44,9 +44,7 @@ func generateInformerSupplier(informerConfig InformerConfig, metrics *reconciler informer, err := operator.NewKubernetesBasedInformer( kind, client, - operator.KubernetesBasedInformerOptions{ - ListWatchOptions: options, - }, + options, ) if err != nil { return nil, err @@ -92,12 +90,14 @@ func New(cfg app.Config) (app.App, error) { KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ InformerSupplier: generateInformerSupplier(appSpecificConfig.InformerConfig, metrics), - ErrorHandler: func(ctx context.Context, err error) { - logging.FromContext(ctx).With("error", err).Error("Informer processing error") - if metrics != nil { - // Use "unknown" for action since top-level informer errors don't have specific actions - metrics.RecordReconcileFailure("unknown", "informer") - } + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).With("error", err).Error("Informer processing error") + if metrics != nil { + // Use "unknown" for action since top-level informer errors don't have specific actions + metrics.RecordReconcileFailure("unknown", "informer") + } + }, }, }, UnmanagedKinds: []simple.AppUnmanagedKind{ diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 6cf0836786d..1652941eba8 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 k8s.io/apimachinery v0.34.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go index 5a615356172..dc73a0301e9 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go @@ -76,7 +76,7 @@ func (c *InvestigationClient) Patch(ctx context.Context, identifier resource.Ide return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationClient) UpdateStatus(ctx context.Context, newStatus InvestigationStatus, opts resource.UpdateOptions) (*Investigation, error) { +func (c *InvestigationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationStatus, opts resource.UpdateOptions) (*Investigation, error) { return c.client.Update(ctx, &Investigation{ TypeMeta: metav1.TypeMeta{ Kind: InvestigationKind().Kind(), @@ -84,6 +84,8 @@ func (c *InvestigationClient) UpdateStatus(ctx context.Context, newStatus Invest }, ObjectMeta: metav1.ObjectMeta{ ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, }, Status: newStatus, }, resource.UpdateOptions{ diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go index 3b63abc5d09..573d743b3cf 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go @@ -76,7 +76,7 @@ func (c *InvestigationIndexClient) Patch(ctx context.Context, identifier resourc return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, newStatus InvestigationIndexStatus, opts resource.UpdateOptions) (*InvestigationIndex, error) { +func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationIndexStatus, opts resource.UpdateOptions) (*InvestigationIndex, error) { return c.client.Update(ctx, &InvestigationIndex{ TypeMeta: metav1.TypeMeta{ Kind: InvestigationIndexKind().Kind(), @@ -84,6 +84,8 @@ func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, newStatus I }, ObjectMeta: metav1.ObjectMeta{ ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, }, Status: newStatus, }, resource.UpdateOptions{ diff --git a/apps/investigations/pkg/apis/investigations_manifest.go b/apps/investigations/pkg/apis/investigations_manifest.go index e4d0dc6fbd9..04c52dbd9ac 100644 --- a/apps/investigations/pkg/apis/investigations_manifest.go +++ b/apps/investigations/pkg/apis/investigations_manifest.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1" ) @@ -27,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "investigations", - Group: "investigations.grafana.app", + AppName: "investigations", + Group: "investigations.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", @@ -50,6 +52,10 @@ var appManifestData = app.ManifestData{ Schema: &versionSchemaInvestigationIndexv0alpha1, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, }, }, } @@ -79,6 +85,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -97,8 +104,22 @@ func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goTyp return goType, exists } +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + type GoTypeAssociator struct{} +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { return ManifestGoTypeAssociator(kind, version) } @@ -108,3 +129,6 @@ func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb str func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { return ManifestCustomRouteQueryAssociator(kind, version, path, verb) } +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/investigations/pkg/app/investigations_app.go b/apps/investigations/pkg/app/investigations_app.go index 3155eb88404..516d278788d 100644 --- a/apps/investigations/pkg/app/investigations_app.go +++ b/apps/investigations/pkg/app/investigations_app.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,8 +19,10 @@ func New(cfg app.Config) (app.App, error) { Name: "investigation", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(_ context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(_ context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 08cebc839ab..4419785540a 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 k8s.io/klog/v2 v2.130.1 @@ -16,7 +16,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/playlist/pkg/apis/playlist_manifest.go b/apps/playlist/pkg/apis/playlist_manifest.go index f27cf6dd635..1baede5a7a4 100644 --- a/apps/playlist/pkg/apis/playlist_manifest.go +++ b/apps/playlist/pkg/apis/playlist_manifest.go @@ -25,8 +25,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "playlist", - Group: "playlist.grafana.app", + AppName: "playlist", + Group: "playlist.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/playlist/pkg/app/app.go b/apps/playlist/pkg/app/app.go index 11cb921dba9..6c9f1762a22 100644 --- a/apps/playlist/pkg/app/app.go +++ b/apps/playlist/pkg/app/app.go @@ -50,8 +50,10 @@ func New(cfg app.Config) (app.App, error) { Name: "playlist", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 1ff6e32794c..35ed57d995b 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 @@ -19,7 +19,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 1b98503c4a5..ca929862af5 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -56,8 +56,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 v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde h1:ydSrBIOCxJQ84+JU+cyYsOLL40QeXrB7rYfsY/ezU4w= diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 2ebfb699452..3965a906139 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "plugins", - Group: "plugins.grafana.app", + AppName: "plugins", + Group: "plugins.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 99fc20a8d2c..5503126704b 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -26,8 +27,10 @@ func New(cfg app.Config) (app.App, error) { Name: "plugins", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 3024a0128df..f21e6a19526 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -13,7 +13,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 8bcea32a115..112d7cb18ab 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -31,8 +31,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/preferences/pkg/apis/preferences_manifest.go b/apps/preferences/pkg/apis/preferences_manifest.go index 3e9e865df6d..3912bc12866 100644 --- a/apps/preferences/pkg/apis/preferences_manifest.go +++ b/apps/preferences/pkg/apis/preferences_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "preferences", - Group: "preferences.grafana.app", + AppName: "preferences", + Group: "preferences.grafana.app", + PreferredVersion: "v1alpha1", Versions: []app.ManifestVersion{ { Name: "v1alpha1", diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 61137e897e5..9a3cd99c066 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -25,7 +25,7 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect @@ -42,7 +42,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.45.0 // indirect + github.com/grafana/grafana-app-sdk v0.46.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 74542a4123f..93819002c42 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= @@ -58,8 +58,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 v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= diff --git a/apps/sdk.mk b/apps/sdk.mk index 529fe145648..2b1f6c8b34d 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.45.0 +APP_SDK_VERSION = v0.46.0 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index e6510b74019..1a6b9ecc824 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.75.1 @@ -18,7 +18,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 1a9beecb06c..712a7a12807 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -35,8 +35,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go new file mode 100644 index 00000000000..c54e3d6f682 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go @@ -0,0 +1,99 @@ +package v1beta1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type KeeperClient struct { + client *resource.TypedClient[*Keeper, *KeeperList] +} + +func NewKeeperClient(client resource.Client) *KeeperClient { + return &KeeperClient{ + client: resource.NewTypedClient[*Keeper, *KeeperList](client, KeeperKind()), + } +} + +func NewKeeperClientFromGenerator(generator resource.ClientGenerator) (*KeeperClient, error) { + c, err := generator.ClientFor(KeeperKind()) + if err != nil { + return nil, err + } + return NewKeeperClient(c), nil +} + +func (c *KeeperClient) Get(ctx context.Context, identifier resource.Identifier) (*Keeper, error) { + return c.client.Get(ctx, identifier) +} + +func (c *KeeperClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*KeeperList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *KeeperClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*KeeperList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *KeeperClient) Create(ctx context.Context, obj *Keeper, opts resource.CreateOptions) (*Keeper, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = KeeperKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *KeeperClient) Update(ctx context.Context, obj *Keeper, opts resource.UpdateOptions) (*Keeper, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *KeeperClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Keeper, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *KeeperClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus KeeperStatus, opts resource.UpdateOptions) (*Keeper, error) { + return c.client.Update(ctx, &Keeper{ + TypeMeta: metav1.TypeMeta{ + Kind: KeeperKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *KeeperClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go new file mode 100644 index 00000000000..241d4cac3c2 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go @@ -0,0 +1,99 @@ +package v1beta1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type SecureValueClient struct { + client *resource.TypedClient[*SecureValue, *SecureValueList] +} + +func NewSecureValueClient(client resource.Client) *SecureValueClient { + return &SecureValueClient{ + client: resource.NewTypedClient[*SecureValue, *SecureValueList](client, SecureValueKind()), + } +} + +func NewSecureValueClientFromGenerator(generator resource.ClientGenerator) (*SecureValueClient, error) { + c, err := generator.ClientFor(SecureValueKind()) + if err != nil { + return nil, err + } + return NewSecureValueClient(c), nil +} + +func (c *SecureValueClient) Get(ctx context.Context, identifier resource.Identifier) (*SecureValue, error) { + return c.client.Get(ctx, identifier) +} + +func (c *SecureValueClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*SecureValueList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *SecureValueClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*SecureValueList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *SecureValueClient) Create(ctx context.Context, obj *SecureValue, opts resource.CreateOptions) (*SecureValue, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = SecureValueKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *SecureValueClient) Update(ctx context.Context, obj *SecureValue, opts resource.UpdateOptions) (*SecureValue, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *SecureValueClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*SecureValue, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *SecureValueClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus SecureValueStatus, opts resource.UpdateOptions) (*SecureValue, error) { + return c.client.Update(ctx, &SecureValue{ + TypeMeta: metav1.TypeMeta{ + Kind: SecureValueKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *SecureValueClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/secret/pkg/apis/secret_manifest.go b/apps/secret/pkg/apis/secret_manifest.go index 512e381f77d..e83afe68aee 100644 --- a/apps/secret/pkg/apis/secret_manifest.go +++ b/apps/secret/pkg/apis/secret_manifest.go @@ -11,13 +11,16 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" ) var appManifestData = app.ManifestData{ - AppName: "secret", - Group: "secret.grafana.app", + AppName: "secret", + Group: "secret.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v1beta1", @@ -37,6 +40,10 @@ var appManifestData = app.ManifestData{ Conversion: false, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, }, }, } @@ -66,6 +73,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -73,3 +81,42 @@ func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (g goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] return goType, exists } + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 3fba5d3439e..7eb91409ca0 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/shorturl go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.1 @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index 9f0fe4068f8..06c31439b6e 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -54,8 +54,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 v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= diff --git a/apps/shorturl/kinds/shorturl.cue b/apps/shorturl/kinds/shorturl.cue index 13938b56b07..ea234173776 100644 --- a/apps/shorturl/kinds/shorturl.cue +++ b/apps/shorturl/kinds/shorturl.cue @@ -20,6 +20,7 @@ shorturl: { response: { url: string } + responseMetadata: typeMeta: false } } } diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_types_gen.go b/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go similarity index 100% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_types_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go diff --git a/apps/shorturl/pkg/apis/shorturl_manifest.go b/apps/shorturl/pkg/apis/shorturl_manifest.go index 5d5b718a681..c4446d924e9 100644 --- a/apps/shorturl/pkg/apis/shorturl_manifest.go +++ b/apps/shorturl/pkg/apis/shorturl_manifest.go @@ -26,8 +26,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "shorturl", - Group: "shorturl.grafana.app", + AppName: "shorturl", + Group: "shorturl.grafana.app", + PreferredVersion: "v1alpha1", Versions: []app.ManifestVersion{ { Name: "v1alpha1", @@ -52,7 +53,7 @@ var appManifestData = app.ManifestData{ Get: &spec3.Operation{ OperationProps: spec3.OperationProps{ - OperationId: "GetGoto", + OperationId: "getGoto", Responses: &spec3.Responses{ ResponsesProps: spec3.ResponsesProps{ diff --git a/apps/shorturl/pkg/app/app.go b/apps/shorturl/pkg/app/app.go index ab9b3477ed2..4c85ef97435 100644 --- a/apps/shorturl/pkg/app/app.go +++ b/apps/shorturl/pkg/app/app.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" shorturlv1alpha1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" @@ -39,8 +40,10 @@ func New(cfg app.Config) (app.App, error) { Name: "shorturl", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/go.mod b/go.mod index c6b9784c092..e00dc78ec9c 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // @grafana/grafana-datasources-core-services github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // @grafana/grafana-datasources-core-services github.com/dustin/go-humanize v1.0.1 // @grafana/observability-traces-and-profiling - github.com/emicklei/go-restful/v3 v3.12.2 // @grafana/grafana-app-platform-squad + github.com/emicklei/go-restful/v3 v3.13.0 // @grafana/grafana-app-platform-squad github.com/fatih/color v1.18.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage @@ -96,7 +96,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.45.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.46.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.45.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.2.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 2f8028bb05b..4f36a619014 100644 --- a/go.sum +++ b/go.sum @@ -1144,8 +1144,8 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -1605,8 +1605,8 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= diff --git a/go.work.sum b/go.work.sum index 8a3cc4f67d2..e3bc1d10c55 100644 --- a/go.work.sum +++ b/go.work.sum @@ -882,6 +882,7 @@ github.com/elastic/lunes v0.1.0 h1:amRtLPjwkWtzDF/RKzcEPMvSsSseLDLW+bnhfNSLRe4= github.com/elastic/lunes v0.1.0/go.mod h1:xGphYIt3XdZRtyWosHQTErsQTd4OP1p9wsbVoHelrd4= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= @@ -1057,6 +1058,8 @@ github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/ github.com/grafana/cog v0.0.37/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= github.com/grafana/cog v0.0.38 h1:V7gRRn/mh7Bg1ptrCxo0bv6K0SnG9TiDZk+3Ppftn6s= github.com/grafana/cog v0.0.38/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= +github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index f7a3cdc5c93..543ace09235 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/aggregator go 1.24.6 require ( - github.com/emicklei/go-restful/v3 v3.12.2 + github.com/emicklei/go-restful/v3 v3.13.0 github.com/grafana/grafana-plugin-sdk-go v0.279.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index f294a97f777..2f6756ed544 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -46,8 +46,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index e084fdf1fad..fe9637801e2 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -17,7 +17,7 @@ require github.com/go-jose/go-jose/v4 v4.1.2 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 2cd16740dd0..11948a2d6ef 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index c039fb3b21c..a6e649494bb 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -28,7 +28,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index a28bdfada90..1025c416476 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -19,8 +19,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 4b1e087e044..91b8f538f49 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -6,7 +6,7 @@ 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.40 + github.com/grafana/cog v0.0.41 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 golang.org/x/tools v0.37.0 diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 8b4652fb9a0..5fd86180488 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -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.40 h1:rPNqOZBV2jXKpi6nJCY5VaoSM4BMSxkN7yuskV4lFxM= -github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41 h1:wszX7YmFkohvLgDy7VDU2XDFNghTdKFvz54zhq9Z+zU= +github.com/grafana/cog v0.0.41/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= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index ace49eacb59..6f7a75648fe 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -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.40 + github.com/grafana/cog v0.0.41 github.com/grafana/cuetsy v0.1.11 github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index 7418051cd4f..c8fff7e7441 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -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.40 h1:rPNqOZBV2jXKpi6nJCY5VaoSM4BMSxkN7yuskV4lFxM= -github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41 h1:wszX7YmFkohvLgDy7VDU2XDFNghTdKFvz54zhq9Z+zU= +github.com/grafana/cog v0.0.41/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= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index dc5c76c2f29..6b046cd9a75 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -33,7 +33,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 2b4eab7619c..7c97cf52cdc 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -57,8 +57,8 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= diff --git a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json index 4816ea0580f..7973627042e 100644 --- a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json @@ -4520,6 +4520,12 @@ "schemas": { "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -4708,6 +4714,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -4967,6 +4979,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -5116,6 +5134,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json index 96ff9719372..dd1b486823e 100644 --- a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json @@ -1160,6 +1160,12 @@ "schemas": { "com.github.grafana.grafana.apps.playlist.pkg.apis.playlist.v0alpha1.Playlist": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json index 0d12c909329..0e646c08d01 100644 --- a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json @@ -2280,6 +2280,12 @@ "schemas": { "com.github.grafana.grafana.apps.alerting.rules.pkg.apis.alerting.v0alpha1.AlertRule": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -2603,6 +2609,12 @@ }, "com.github.grafana.grafana.apps.alerting.rules.pkg.apis.alerting.v0alpha1.RecordingRule": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json index 9807d9c13b9..574ce2f474a 100644 --- a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json @@ -1220,6 +1220,12 @@ }, "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts b/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts index 347f6d2b86a..7a42a89de07 100644 --- a/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts @@ -308,11 +308,11 @@ export type PlaylistStatus = { }; export type Playlist = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: PlaylistSpec; + kind: string; + metadata: ObjectMeta; + spec: PlaylistSpec; status?: PlaylistStatus; }; export type ListMeta = { diff --git a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts index 1555dae0e46..6537a5061fc 100644 --- a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts @@ -931,11 +931,11 @@ export type AlertRuleStatus = { }; export type AlertRule = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: AlertRuleSpec; + kind: string; + metadata: ObjectMeta; + spec: AlertRuleSpec; status?: AlertRuleStatus; }; export type ListMeta = { @@ -1070,11 +1070,11 @@ export type RecordingRuleStatus = { }; export type RecordingRule = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: RecordingRuleSpec; + kind: string; + metadata: ObjectMeta; + spec: RecordingRuleSpec; status?: RecordingRuleStatus; }; export type RecordingRuleList = { diff --git a/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts b/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts index cfb88c9224b..9cabcb8ab7b 100644 --- a/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts +++ b/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts @@ -524,11 +524,11 @@ export type ShortUrlStatus = { }; export type ShortUrl = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: ShortUrlSpec; + kind: string; + metadata: ObjectMeta; + spec: ShortUrlSpec; status?: ShortUrlStatus; }; export type ListMeta = { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index b9187f7ca4c..eeaceaa298f 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -48,6 +48,9 @@ export const createShortLink = async function (path: string) { const result = await dispatch( generatedAPI.endpoints.createShortUrl.initiate({ shortUrl: { + apiVersion: 'shorturl.grafana.app/v1alpha1', + kind: 'Playlist', + metadata: {}, spec: { path: getRelativeURLPath(path), }, diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index a08012a81c3..657a97560c3 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -12,6 +12,8 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ })); const mockPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', @@ -28,6 +30,8 @@ const mockPlaylist: Playlist = { }; const mockEmptyPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', @@ -102,6 +106,8 @@ describe('PlaylistForm', () => { await userEvent.click(screen.getByRole('button', { name: /save/i })); expect(onSubmitMock).toHaveBeenCalledTimes(1); expect(onSubmitMock).toHaveBeenCalledWith({ + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', diff --git a/public/app/features/playlist/PlaylistSrv.test.ts b/public/app/features/playlist/PlaylistSrv.test.ts index 9b7bed662a7..8cb7559bbf4 100644 --- a/public/app/features/playlist/PlaylistSrv.test.ts +++ b/public/app/features/playlist/PlaylistSrv.test.ts @@ -22,6 +22,8 @@ jest.mock('./utils', () => ({ })); const mockPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { interval: '1s', title: 'The display', diff --git a/public/app/features/playlist/utils.ts b/public/app/features/playlist/utils.ts index d9c9635ee70..2b46984787c 100644 --- a/public/app/features/playlist/utils.ts +++ b/public/app/features/playlist/utils.ts @@ -71,6 +71,8 @@ export async function loadDashboards(items: PlaylistItemUI[]): Promise