diff --git a/.betterer.results b/.betterer.results index 88567ed76b6..2539de2a37f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -794,8 +794,7 @@ exports[`better eslint`] = { "public/app/app.ts:5381": [ [0, 0, 0, "\'@grafana/runtime/src/components/PanelDataErrorView\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/runtime/src/components/PanelRenderer\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], - [0, 0, 0, "\'@grafana/runtime/src/unstable\' import is restricted from being used by a pattern. Import from the public export instead.", "3"] + [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "2"] ], "public/app/core/TableModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -898,6 +897,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], + "public/app/core/components/NestedFolderPicker/FolderRepo.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + ], "public/app/core/components/OptionsUI/color.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], @@ -1010,6 +1012,12 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`profiler\`)", "6"], [0, 0, 0, "Do not re-export imported variable (\`updateLegendValues\`)", "7"] ], + "public/app/core/internationalization/index.test.tsx:5381": [ + [0, 0, 0, "\'@grafana/runtime/src/unstable\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] + ], + "public/app/core/internationalization/index.tsx:5381": [ + [0, 0, 0, "\'@grafana/runtime/src/unstable\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] + ], "public/app/core/navigation/GrafanaRouteError.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] @@ -1128,127 +1136,25 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"] ], - "public/app/features/admin/AdminEditOrgPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], "public/app/features/admin/AdminFeatureTogglesTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], - "public/app/features/admin/AdminOrgsTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "public/app/features/admin/OrgRolePicker.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/admin/UpgradePage.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "12"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "13"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "14"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "15"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "16"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "17"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "18"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "19"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "20"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "21"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "22"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "23"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "24"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "25"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "26"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "27"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "28"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "29"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "30"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "31"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "32"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "33"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "34"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "35"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "36"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "37"] - ], - "public/app/features/admin/UserCreatePage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], - "public/app/features/admin/UserLdapSyncInfo.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/admin/UserListAdminPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/admin/UserListAnonymousPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/admin/UserListPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], - "public/app/features/admin/UserOrgs.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] - ], - "public/app/features/admin/UserProfile.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"] - ], - "public/app/features/admin/UserSessions.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/admin/Users/OrgUsersTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/admin/Users/UsersTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] - ], - "public/app/features/admin/ldap/LdapConnectionStatus.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/admin/ldap/LdapDrawer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], - "public/app/features/admin/ldap/LdapPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/admin/ldap/LdapSettingsPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], "public/app/features/alerting/routes.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -1966,11 +1872,12 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], "public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -2013,7 +1920,7 @@ exports[`better eslint`] = { ], "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -2616,74 +2523,33 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/features/api-keys/ApiKeysActionBar.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/api-keys/ApiKeysPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] - ], - "public/app/features/api-keys/ApiKeysTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] - ], - "public/app/features/api-keys/MigrateToServiceAccountsCard.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] ], + "public/app/features/api-keys/MigrateToServiceAccountsCard.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], "public/app/features/auth-config/AuthDrawer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/auth-config/AuthProvidersListPage.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/types/config\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/auth-config/ProviderConfigForm.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] - ], - "public/app/features/auth-config/components/ConfigureAuthCTA.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], - "public/app/features/auth-config/components/ServerDiscoveryModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/auth-config/fields.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2704,14 +2570,26 @@ exports[`better eslint`] = { "public/app/features/browse-dashboards/api/browseDashboardsAPI.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`@reduxjs/toolkit/query/react\`)", "0"] ], - "public/app/features/browse-dashboards/components/BrowseActions/DescendantCount.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/browse-dashboards/components/BrowseView.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "public/app/features/browse-dashboards/components/NameCell.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + "public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "13"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "14"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "15"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "16"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "17"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "18"] ], "public/app/features/browse-dashboards/state/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], @@ -2735,9 +2613,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/connections/components/ConnectionsRedirectNotice/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] @@ -2765,16 +2641,13 @@ exports[`better eslint`] = { "public/app/features/connections/tabs/ConnectData/NoAccessModal/NoAccessModal.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/connections/tabs/ConnectData/NoAccessModal/index.tsx:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] ], "public/app/features/connections/tabs/ConnectData/Search/Search.tsx:5381": [ - [0, 0, 0, "Do not use the t() function outside of a component or function", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] + [0, 0, 0, "Do not use the t() function outside of a component or function", "0"] ], "public/app/features/connections/tabs/ConnectData/Search/index.tsx:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] @@ -4030,98 +3903,41 @@ exports[`better eslint`] = { "public/app/features/dimensions/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/explore/ContentOutline/ContentOutlineItemButton.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/explore/CorrelationEditorModeBar.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/explore/CorrelationHelper.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], "public/app/features/explore/CorrelationTransformationAddModal.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/explore/CorrelationUnsavedChangesModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], - "public/app/features/explore/Explore.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/ExploreRunQueryButton.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "public/app/features/explore/ExploreToolbar.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/FeatureTogglePage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/LiveTailButton.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/explore/Logs/LiveLogs.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/Logs/Logs.test.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/organize\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], "public/app/features/explore/Logs/Logs.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/explore/Logs/LogsColumnSearch.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/features/explore/Logs/LogsContainer.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/explore/Logs/LogsFeedback.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -4129,19 +3945,8 @@ exports[`better eslint`] = { "public/app/features/explore/Logs/LogsMetaRow.test.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/organize\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], - "public/app/features/explore/Logs/LogsMetaRow.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] - ], "public/app/features/explore/Logs/LogsSamplePanel.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/Logs/LogsTable.test.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/organize\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] @@ -4149,43 +3954,24 @@ exports[`better eslint`] = { "public/app/features/explore/Logs/LogsTableAvailableFields.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/features/explore/Logs/LogsTableEmptyFields.tsx:5381": [ + "public/app/features/explore/Logs/LogsTableMultiSelect.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/Logs/LogsTableMultiSelect.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/explore/Logs/LogsTableNavField.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/Logs/LogsTableWrap.test.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/organize\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], "public/app/features/explore/Logs/LogsTableWrap.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/Logs/LogsVolumePanelList.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/MetaInfoText.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/NoDataSourceCallToAction.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/explore/NodeGraph/NodeGraphContainer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -4196,8 +3982,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/explore/PrometheusListView/RawListItem.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/PrometheusListView/RawListItemAttributes.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -4217,21 +4002,12 @@ exports[`better eslint`] = { "public/app/features/explore/ShortLinkButtonMenu.tsx:5381": [ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"] ], - "public/app/features/explore/SupplementaryResultError.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/explore/TraceView/TraceView.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], - "public/app/features/explore/TraceView/TraceViewContainer.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -4239,10 +4015,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -4258,35 +4031,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "12"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "13"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "14"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "15"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "16"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "17"] - ], - "public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"] - ], - "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -4310,43 +4055,20 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./TimelineHeaderRow\`)", "0"] ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`../utils/date\`)", "0"] ], - "public/app/features/explore/TraceView/components/common/SearchBarInput.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], "public/app/features/explore/TraceView/components/demo/trace-generators.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -4384,13 +4106,7 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] ], "public/app/features/explore/TraceView/components/types/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`../settings/SpanBarSettings\`)", "0"], @@ -4402,11 +4118,7 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DraggableManagerDemo\`)", "0"] @@ -4418,31 +4130,18 @@ exports[`better eslint`] = { ], "public/app/features/explore/TraceView/createSpanLink.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"] + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/explore/extensions/ConfirmNavigationModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], - "public/app/features/explore/extensions/toolbar/BasicExtensions.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/explore/extensions/toolbar/QuerylessAppsExtensions.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/hooks/useStateSync/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./external.utils\`)", "0"] ], "public/app/features/explore/spec/helper/setup.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/explore/state/main.test.ts:5381": [ [0, 0, 0, "\'@grafana/data/src/utils/url\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] @@ -4598,39 +4297,17 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] ], - "public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] - ], - "public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] + [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/live/LiveConnectionWarning.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] @@ -4779,29 +4456,13 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/org/NewOrgPage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], - "public/app/features/org/OrgProfile.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/org/SelectOrgPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/org/UserInviteForm.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/org/UserInvitePage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -4856,16 +4517,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/playlist/StartModal.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/plugins/admin/__mocks__/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./localPlugin.mock\`)", "0"], @@ -4928,10 +4580,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] ], "public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./InstallControlsButton\`)", "0"], @@ -5113,6 +4764,291 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], + "public/app/features/provisioning/Config/ConfigForm.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "12"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "13"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "14"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "15"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "16"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "17"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "18"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "19"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "20"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "21"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "22"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "23"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "24"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "25"] + ], + "public/app/features/provisioning/Config/ConfigFormGithubCollapse.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"] + ], + "public/app/features/provisioning/File/FileHistoryPage.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], + "public/app/features/provisioning/File/FileStatusPage.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] + ], + "public/app/features/provisioning/File/FilesView.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + ], + "public/app/features/provisioning/GettingStarted/EnhancedFeatures.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + ], + "public/app/features/provisioning/GettingStarted/FeaturesList.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + ], + "public/app/features/provisioning/GettingStarted/GettingStarted.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], + "public/app/features/provisioning/GettingStarted/SetupModal.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], + "public/app/features/provisioning/HomePage.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + ], + "public/app/features/provisioning/Job/JobStatus.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + ], + "public/app/features/provisioning/Job/RecentJobs.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + ], + "public/app/features/provisioning/Repository/CheckRepository.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + ], + "public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] + ], + "public/app/features/provisioning/Repository/EditRepositoryPage.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/Repository/RepositoryActions.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/Repository/RepositoryCard.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], + "public/app/features/provisioning/Repository/RepositoryHealth.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + ], + "public/app/features/provisioning/Repository/RepositoryOverview.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] + ], + "public/app/features/provisioning/Repository/RepositoryResources.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + ], + "public/app/features/provisioning/Repository/RepositoryStatusPage.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + ], + "public/app/features/provisioning/Repository/SyncRepository.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], + "public/app/features/provisioning/Shared/FolderRepositoryList.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + ], + "public/app/features/provisioning/Wizard/BootstrapStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"] + ], + "public/app/features/provisioning/Wizard/ConnectStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"] + ], + "public/app/features/provisioning/Wizard/FinishStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "12"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "13"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "14"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "15"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "16"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "17"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "18"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "19"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "20"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "21"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "22"] + ], + "public/app/features/provisioning/Wizard/MigrateStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + ], + "public/app/features/provisioning/Wizard/PullStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + ], + "public/app/features/provisioning/Wizard/Stepper.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + ], + "public/app/features/provisioning/Wizard/WizardContent.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/hooks/index.ts:5381": [ + [0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepositoryFile\`)", "0"], + [0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepository\`)", "1"], + [0, 0, 0, "Do not re-export imported variable (\`./useGetResourceRepository\`)", "2"], + [0, 0, 0, "Do not re-export imported variable (\`./useIsProvisionedNG\`)", "3"], + [0, 0, 0, "Do not re-export imported variable (\`./usePullRequestParam\`)", "4"], + [0, 0, 0, "Do not re-export imported variable (\`./useRepositoryJobs\`)", "5"], + [0, 0, 0, "Do not re-export imported variable (\`./useRepositoryList\`)", "6"] + ], + "public/app/features/provisioning/types.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -5350,11 +5286,7 @@ exports[`better eslint`] = { ], "public/app/features/teams/CreateTeam.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "public/app/features/teams/TeamGroupSync.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -5362,31 +5294,17 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "public/app/features/teams/TeamList.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] ], "public/app/features/teams/TeamSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/teams/state/reducers.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -6727,9 +6645,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/tempo/SearchTraceQLEditor/DurationInput.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], - "public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], @@ -6959,6 +6874,11 @@ exports[`better eslint`] = { "public/app/plugins/panel/histogram/utils.ts:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/histogram\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], + "public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "public/app/plugins/panel/live/LivePanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/.bingo/README.md b/.bingo/README.md index 1d8a1360ccf..7a5c2d4f6da 100644 --- a/.bingo/README.md +++ b/.bingo/README.md @@ -2,13 +2,13 @@ This is directory which stores Go modules with pinned buildable package that is used within this repository, managed by https://github.com/bwplotka/bingo. -- Run `bingo get` to install all tools having each own module file in this directory. -- Run `bingo get ` to install that have own module file in this directory. -- For Makefile: Make sure to put `include .bingo/Variables.mk` in your Makefile, then use $() variable where is the .bingo/.mod. -- For shell: Run `source .bingo/variables.env` to source all environment variable for each tool. -- For go: Import `.bingo/variables.go` to for variable names. -- See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies. +* Run `bingo get` to install all tools having each own module file in this directory. +* Run `bingo get ` to install that have own module file in this directory. +* For Makefile: Make sure to put `include .bingo/Variables.mk` in your Makefile, then use $() variable where is the .bingo/.mod. +* For shell: Run `source .bingo/variables.env` to source all environment variable for each tool. +* For go: Import `.bingo/variables.go` to for variable names. +* See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies. ## Requirements -- Go 1.14+ +* Go 1.14+ diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 80b5946b497..4160da8e0f7 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -5,68 +5,21 @@ GOPATH ?= $(shell go env GOPATH) GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin GO ?= $(shell which go) -# Add this near the top of the file, after the initial variable definitions -ifndef VARIABLES_MK -VARIABLES_MK := 1 - # Below generated variables ensure that every time a tool under each variable is invoked, the correct version # will be used; reinstalling only if needed. -# For example for bra variable: +# For example for drone variable: # # In your main Makefile (for non array binaries): # #include .bingo/Variables.mk # Assuming -dir was set to .bingo . # -#command: $(BRA) -# @echo "Running bra" -# @$(BRA) +#command: $(DRONE) +# @echo "Running drone" +# @$(DRONE) # -BRA := $(GOBIN)/bra-v0.0.0-20200517080246-1e3013ecaff8 -$(BRA): $(BINGO_DIR)/bra.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/bra-v0.0.0-20200517080246-1e3013ecaff8" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=bra.mod -o=$(GOBIN)/bra-v0.0.0-20200517080246-1e3013ecaff8 "github.com/unknwon/bra" - -COG := $(GOBIN)/cog-v0.0.15 -$(COG): $(BINGO_DIR)/cog.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/cog-v0.0.15" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=cog.mod -o=$(GOBIN)/cog-v0.0.15 "github.com/grafana/cog/cmd/cli" - -CUE := $(GOBIN)/cue-v0.5.0 -$(CUE): $(BINGO_DIR)/cue.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/cue-v0.5.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=cue.mod -o=$(GOBIN)/cue-v0.5.0 "cuelang.org/go/cmd/cue" - DRONE := $(GOBIN)/drone-v1.5.0 $(DRONE): $(BINGO_DIR)/drone.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/drone-v1.5.0" @cd $(BINGO_DIR) && GOWORK=off CGO_ENABLED=0 $(GO) build -mod=mod -modfile=drone.mod -o=$(GOBIN)/drone-v1.5.0 "github.com/drone/drone-cli/drone" -GOLANGCI_LINT := $(GOBIN)/golangci-lint-v1.62.0 -$(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/golangci-lint-v1.62.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v1.62.0 "github.com/golangci/golangci-lint/cmd/golangci-lint" - -JB := $(GOBIN)/jb-v0.5.1 -$(JB): $(BINGO_DIR)/jb.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/jb-v0.5.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=jb.mod -o=$(GOBIN)/jb-v0.5.1 "github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb" - -LEFTHOOK := $(GOBIN)/lefthook-v1.4.8 -$(LEFTHOOK): $(BINGO_DIR)/lefthook.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/lefthook-v1.4.8" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=lefthook.mod -o=$(GOBIN)/lefthook-v1.4.8 "github.com/evilmartians/lefthook" - -SWAGGER := $(GOBIN)/swagger-v0.30.6-0.20240310114303-db51e79a0e37 -$(SWAGGER): $(BINGO_DIR)/swagger.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/swagger-v0.30.6-0.20240310114303-db51e79a0e37" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=swagger.mod -o=$(GOBIN)/swagger-v0.30.6-0.20240310114303-db51e79a0e37 "github.com/go-swagger/go-swagger/cmd/swagger" - -endif diff --git a/.bingo/bra.mod b/.bingo/bra.mod deleted file mode 100644 index 0a64dc1765f..00000000000 --- a/.bingo/bra.mod +++ /dev/null @@ -1,7 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.17 - -replace golang.org/x/sys => golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c - -require github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 diff --git a/.bingo/bra.sum b/.bingo/bra.sum deleted file mode 100644 index d4e9da39ec8..00000000000 --- a/.bingo/bra.sum +++ /dev/null @@ -1,30 +0,0 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= -github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= -github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= -github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -golang.org/x/sys v0.0.0-20191020152052-9984515f0562 h1:wOweSabW7qssfcg63CEDHHA4zyoqRlGU6eYV7IUMCq0= -golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= \ No newline at end of file diff --git a/.bingo/cog.mod b/.bingo/cog.mod deleted file mode 100644 index c6c7b46ef7d..00000000000 --- a/.bingo/cog.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.23.4 - -require github.com/grafana/cog v0.0.15 // cmd/cli diff --git a/.bingo/cog.sum b/.bingo/cog.sum deleted file mode 100644 index 8280a813d47..00000000000 --- a/.bingo/cog.sum +++ /dev/null @@ -1,86 +0,0 @@ -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= -cuelang.org/go v0.11.0 h1:2af2nhipqlUHtXk2dtOP5xnMm1ObGvKqIsJUJL1sRE4= -cuelang.org/go v0.11.0/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= -github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= -github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= -github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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.14 h1:sBK89oSu9BK4S9l3G9ewVJnGYnNQJTHFBC/01DZDRZs= -github.com/grafana/cog v0.0.14/go.mod h1:HwJbc60fZ+viayROClLGdDwO5w/JjBOpO9wjGnAfMLc= -github.com/grafana/cog v0.0.15 h1:e2pMY+Hf2nS22HcKJuguEzl0BVmV9DSINwCfWt+dFZQ= -github.com/grafana/cog v0.0.15/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY= -github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.bingo/cue.mod b/.bingo/cue.mod deleted file mode 100644 index 6e13b6d3eec..00000000000 --- a/.bingo/cue.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.19 - -require cuelang.org/go v0.5.0 // cmd/cue diff --git a/.bingo/cue.sum b/.bingo/cue.sum deleted file mode 100644 index 5e9012b2946..00000000000 --- a/.bingo/cue.sum +++ /dev/null @@ -1,58 +0,0 @@ -cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= -cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= -cuelang.org/go v0.5.0-beta.2 h1:am5M7jGvNTJ0rnjrFNyvE7fucL/wRqb0emK4XxdThQI= -cuelang.org/go v0.5.0-beta.2/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= -cuelang.org/go v0.5.0 h1:D6N0UgTGJCOxFKU8RU+qYvavKNsVc/+ZobmifStVJzU= -cuelang.org/go v0.5.0/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd/v2 v2.0.1 h1:y1Rh3tEU89D+7Tgbw+lp52T6p/GJLpDmNvr10UWqLTE= -github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= -github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/emicklei/proto v1.6.15 h1:XbpwxmuOPrdES97FrSfpyy67SSCV/wBIKXqgJzh6hNw= -github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= -github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= -github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc h1:gSVONBi2HWMFXCa9jFdYvYk7IwW/mTLxWOF7rXS4LO0= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= -golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0= -golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.0.0-20200612220849-54c614fe050c h1:g6oFfz6Cmw68izP3xsdud3Oxu145IPkeFzyRg58AKHM= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.bingo/golangci-lint.mod b/.bingo/golangci-lint.mod deleted file mode 100644 index 73e12b0e2ed..00000000000 --- a/.bingo/golangci-lint.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.23 - -require github.com/golangci/golangci-lint v1.62.0 // cmd/golangci-lint diff --git a/.bingo/golangci-lint.sum b/.bingo/golangci-lint.sum deleted file mode 100644 index 6e18a455fa9..00000000000 --- a/.bingo/golangci-lint.sum +++ /dev/null @@ -1,1174 +0,0 @@ -4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= -4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= -4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= -4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/4meepo/tagalign v1.3.3 h1:ZsOxcwGD/jP4U/aw7qeWu58i7dwYemfy5Y+IF1ACoNw= -github.com/4meepo/tagalign v1.3.3/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= -github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= -github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= -github.com/Abirdcfly/dupword v0.0.14 h1:3U4ulkc8EUo+CaT105/GJ1BQwtgyj6+VaBVbAX11Ba8= -github.com/Abirdcfly/dupword v0.0.14/go.mod h1:VKDAbxdY8YbKUByLGg8EETzYSuC4crm9WwI6Y3S0cLI= -github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY= -github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM= -github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= -github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= -github.com/Antonboom/errname v0.1.12 h1:oh9ak2zUtsLp5oaEd/erjB4GPu9w19NyoIskZClDcQY= -github.com/Antonboom/errname v0.1.12/go.mod h1:bK7todrzvlaZoQagP1orKzWXv59X/x0W0Io2XT1Ssro= -github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= -github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= -github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= -github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= -github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTow= -github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= -github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= -github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= -github.com/Antonboom/nilnil v1.0.0 h1:n+v+B12dsE5tbAqRODXmEKfZv9j2KcTBrp+LkoM4HZk= -github.com/Antonboom/nilnil v1.0.0/go.mod h1:fDJ1FSFoLN6yoG65ANb1WihItf6qt9PJVTn/s2IrcII= -github.com/Antonboom/testifylint v1.2.0 h1:015bxD8zc5iY8QwTp4+RG9I4kIbqwvGX9TrBbb7jGdM= -github.com/Antonboom/testifylint v1.2.0/go.mod h1:rkmEqjqVnHDRNsinyN6fPSLnoajzFwsCcguJgwADBkw= -github.com/Antonboom/testifylint v1.3.0 h1:UiqrddKs1W3YK8R0TUuWwrVKlVAnS07DTUVWWs9c+y4= -github.com/Antonboom/testifylint v1.3.0/go.mod h1:NV0hTlteCkViPW9mSR4wEMfwp+Hs1T3dY60bkvSfhpM= -github.com/Antonboom/testifylint v1.3.1 h1:Uam4q1Q+2b6H7gvk9RQFw6jyVDdpzIirFOOrbs14eG4= -github.com/Antonboom/testifylint v1.3.1/go.mod h1:NV0hTlteCkViPW9mSR4wEMfwp+Hs1T3dY60bkvSfhpM= -github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck= -github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA= -github.com/Antonboom/testifylint v1.5.0 h1:dlUIsDMtCrZWUnvkaCz3quJCoIjaGi41GzjPBGkkJ8A= -github.com/Antonboom/testifylint v1.5.0/go.mod h1:wqaJbu0Blb5Wag2wv7Z5xt+CIV+eVLxtGZrlK13z3AE= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.2.2 h1:OrFlsDdOj9hW/oBEJBNSuH7QWf+E9WPVHw+x52bXVbk= -github.com/Crocmagnon/fatcontext v0.2.2/go.mod h1:WSn/c/+MMNiD8Pri0ahRj0o9jVpeowzavOQplBJw6u0= -github.com/Crocmagnon/fatcontext v0.4.0 h1:4ykozu23YHA0JB6+thiuEv7iT6xq995qS1vcuWZq0tg= -github.com/Crocmagnon/fatcontext v0.4.0/go.mod h1:ZtWrXkgyfsYPzS6K3O88va6t2GEglG93vnII/F94WC0= -github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= -github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0/go.mod h1:Nl76DrGNJTA1KJ0LePKBw/vznBX1EHbAZX8mwjR82nI= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= -github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= -github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= -github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= -github.com/alecthomas/go-check-sumtype v0.2.0 h1:Bo+e4DFf3rs7ME9w/0SU/g6nmzJaphduP8Cjiz0gbwY= -github.com/alecthomas/go-check-sumtype v0.2.0/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= -github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= -github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= -github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= -github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= -github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= -github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= -github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= -github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= -github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= -github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= -github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= -github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= -github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= -github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bombsimon/wsl/v4 v4.2.1 h1:Cxg6u+XDWff75SIFFmNsqnIOgob+Q9hG6y/ioKbRFiM= -github.com/bombsimon/wsl/v4 v4.2.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= -github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= -github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= -github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= -github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= -github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= -github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= -github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= -github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= -github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= -github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= -github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= -github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= -github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= -github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= -github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= -github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= -github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= -github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= -github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= -github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/ckaznocha/intrange v0.1.0 h1:ZiGBhvrdsKpoEfzh9CjBfDSZof6QB0ORY5tXasUtiew= -github.com/ckaznocha/intrange v0.1.0/go.mod h1:Vwa9Ekex2BrEQMg6zlrWwbs/FtYw7eS5838Q7UjK7TQ= -github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI= -github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= -github.com/ckaznocha/intrange v0.2.0 h1:FykcZuJ8BD7oX93YbO1UY9oZtkRbp+1/kJcDjkefYLs= -github.com/ckaznocha/intrange v0.2.0/go.mod h1:r5I7nUlAAG56xmkOpw4XVr16BXhwYTUdcuRFeevn1oE= -github.com/ckaznocha/intrange v0.2.1 h1:M07spnNEQoALOJhwrImSrJLaxwuiQK+hA2DeajBlwYk= -github.com/ckaznocha/intrange v0.2.1/go.mod h1:7NEhVyf8fzZO5Ds7CRaqPEm52Ut83hsTiL5zbER/HYk= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= -github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= -github.com/daixiang0/gci v0.12.3 h1:yOZI7VAxAGPQmkb1eqt5g/11SUlwoat1fSblGLmdiQc= -github.com/daixiang0/gci v0.12.3/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= -github.com/daixiang0/gci v0.13.4 h1:61UGkmpoAcxHM2hhNkZEf5SzwQtWJXTSws7jaPyqwlw= -github.com/daixiang0/gci v0.13.4/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= -github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= -github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= -github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= -github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= -github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= -github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= -github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= -github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbxOK4Ug= -github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= -github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/ghostiam/protogetter v0.3.8 h1:LYcXbYvybUyTIxN2Mj9h6rHrDZBDwZloPoKctWrFyJY= -github.com/ghostiam/protogetter v0.3.8/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= -github.com/go-critic/go-critic v0.11.2 h1:81xH/2muBphEgPtcwH1p6QD+KzXl2tMSi3hXjBSxDnM= -github.com/go-critic/go-critic v0.11.2/go.mod h1:OePaicfjsf+KPy33yq4gzv6CO7TEQ9Rom6ns1KsJnl8= -github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= -github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= -github.com/go-critic/go-critic v0.11.5 h1:TkDTOn5v7EEngMxu8KbuFqFR43USaaH8XRJLz1jhVYA= -github.com/go-critic/go-critic v0.11.5/go.mod h1:wu6U7ny9PiaHaZHcvMDmdysMqvDem162Rh3zWTrqk8M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= -github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= -github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= -github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= -github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= -github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= -github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= -github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= -github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= -github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= -github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= -github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= -github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= -github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= -github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= -github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= -github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= -github.com/golangci/golangci-lint v1.57.1 h1:cqhpzkzjDwdN12rfMf1SUyyKyp88a1SltNqEYGS0nJw= -github.com/golangci/golangci-lint v1.57.1/go.mod h1:zLcHhz3NHc88T5zV2j75lyc0zH3LdOPOybblYa4p0oI= -github.com/golangci/golangci-lint v1.59.0 h1:st69YDnAH/v2QXDcgUaZ0seQajHScPALBVkyitYLXEk= -github.com/golangci/golangci-lint v1.59.0/go.mod h1:QNA32UWdUdHXnu+Ap5/ZU4WVwyp2tL94UxEXrSErjg0= -github.com/golangci/golangci-lint v1.59.1 h1:CRRLu1JbhK5avLABFJ/OHVSQ0Ie5c4ulsOId1h3TTks= -github.com/golangci/golangci-lint v1.59.1/go.mod h1:jX5Oif4C7P0j9++YB2MMJmoNrb01NJ8ITqKWNLewThg= -github.com/golangci/golangci-lint v1.60.1 h1:DRKNqNTQRLBJZ1il5u4fvgLQCjQc7QFs0DbhksJtVJE= -github.com/golangci/golangci-lint v1.60.1/go.mod h1:jDIPN1rYaIA+ijp9OZcUmUCoQOtZ76pOlFbi15FlLJY= -github.com/golangci/golangci-lint v1.61.0 h1:VvbOLaRVWmyxCnUIMTbf1kDsaJbTzH20FAMXTAlQGu8= -github.com/golangci/golangci-lint v1.61.0/go.mod h1:e4lztIrJJgLPhWvFPDkhiMwEFRrWlmFbrZea3FsJyN8= -github.com/golangci/golangci-lint v1.62.0 h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI= -github.com/golangci/golangci-lint v1.62.0/go.mod h1:jtoOhQcKTz8B6dGNFyfQV3WZkQk+YvBDewDtNpiAJts= -github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= -github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= -github.com/golangci/misspell v0.5.1 h1:/SjR1clj5uDjNLwYzCahHwIOPmQgoH04AyQIiWGbhCM= -github.com/golangci/misspell v0.5.1/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= -github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= -github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= -github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= -github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= -github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= -github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= -github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs= -github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= -github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= -github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= -github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= -github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= -github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jgautheron/goconst v1.7.0 h1:cEqH+YBKLsECnRSd4F4TK5ri8t/aXtt/qoL0Ft252B0= -github.com/jgautheron/goconst v1.7.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= -github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jjti/go-spancheck v0.5.3 h1:vfq4s2IB8T3HvbpiwDTYgVPj1Ze/ZSXrTtaZRTc7CuM= -github.com/jjti/go-spancheck v0.5.3/go.mod h1:eQdOX1k3T+nAKvZDyLC3Eby0La4dZ+I19iOl5NzSPFE= -github.com/jjti/go-spancheck v0.6.1 h1:ZK/wE5Kyi1VX3PJpUO2oEgeoI4FWOUm7Shb2Gbv5obI= -github.com/jjti/go-spancheck v0.6.1/go.mod h1:vF1QkOO159prdo6mHRxak2CpzDpHAfKiPUDP/NeRnX8= -github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= -github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= -github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= -github.com/karamaru-alpha/copyloopvar v1.0.8 h1:gieLARwuByhEMxRwM3GRS/juJqFbLraftXIKDDNJ50Q= -github.com/karamaru-alpha/copyloopvar v1.0.8/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= -github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= -github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= -github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= -github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= -github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= -github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= -github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= -github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= -github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= -github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= -github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= -github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= -github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= -github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= -github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= -github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= -github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= -github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= -github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= -github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= -github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= -github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= -github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= -github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= -github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= -github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= -github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= -github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= -github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= -github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= -github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= -github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= -github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= -github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= -github.com/mgechev/revive v1.5.0 h1:oaSmjA7rP8+HyoRuCgC531VHwnLH1AlJdjj+1AnQceQ= -github.com/mgechev/revive v1.5.0/go.mod h1:L6T3H8EoerRO86c7WuGpvohIUmiploGiyoYbtIWFmV8= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= -github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= -github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= -github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= -github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= -github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= -github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.16.1 h1:uDIPSxgVHZ7PgbJElRDGzymkXH+JaF7mjew+Thjnt6Q= -github.com/nunnatsa/ginkgolinter v0.16.1/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= -github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= -github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= -github.com/nunnatsa/ginkgolinter v0.18.0 h1:ZXO1wKhPg3A6LpbN5dMuqwhfOjN5c3ous8YdKOuqk9k= -github.com/nunnatsa/ginkgolinter v0.18.0/go.mod h1:vPrWafSULmjMGCMsfGA908if95VnHQNAahvSBOjTuWs= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v1.4.8 h1:jiEjKDH33ouFktyez7sckv6pHWif9B7SuS8cutDXFHw= -github.com/polyfloyd/go-errorlint v1.4.8/go.mod h1:NNCxFcFjZcw3xNjVdCchERkEM6Oz7wta2XJVxRftwO4= -github.com/polyfloyd/go-errorlint v1.5.1 h1:5gHxDjLyyWij7fhfrjYNNlHsUNQeyx0LFQKUelO3RBo= -github.com/polyfloyd/go-errorlint v1.5.1/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= -github.com/polyfloyd/go-errorlint v1.5.2 h1:SJhVik3Umsjh7mte1vE0fVZ5T1gznasQG3PV7U5xFdA= -github.com/polyfloyd/go-errorlint v1.5.2/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= -github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= -github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs= -github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= -github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= -github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= -github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/raeperd/recvcheck v0.1.2 h1:SjdquRsRXJc26eSonWIo8b7IMtKD3OAT2Lb5G3ZX1+4= -github.com/raeperd/recvcheck v0.1.2/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.3.1 h1:fH+fUg+ngsQO0ruZXXHnA/2aNllWA1whly4a6UvyzGE= -github.com/ryancurrah/gomodguard v1.3.1/go.mod h1:DGFHzEhi6iJ0oIDfMuo3TgrS+L9gZvrEfmjjuelnRU0= -github.com/ryancurrah/gomodguard v1.3.2 h1:CuG27ulzEB1Gu5Dk5gP8PFxSOZ3ptSdP5iI/3IXxM18= -github.com/ryancurrah/gomodguard v1.3.2/go.mod h1:LqdemiFomEjcxOqirbQCb3JFvSxH2JUYMerTFd3sF2o= -github.com/ryancurrah/gomodguard v1.3.3 h1:eiSQdJVNr9KTNxY2Niij8UReSwR8Xrte3exBrAZfqpg= -github.com/ryancurrah/gomodguard v1.3.3/go.mod h1:rsKQjj4l3LXe8N344Ow7agAy5p9yjsWOtRzUMYmA0QY= -github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= -github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= -github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= -github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= -github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= -github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= -github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.25.0 h1:IK8SI2QyFzy/2OD2PYnhy84dpfNo9qADrRt6LH8vSzU= -github.com/sashamelentyev/usestdlibvars v1.25.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/sashamelentyev/usestdlibvars v1.26.0 h1:LONR2hNVKxRmzIrZR0PhSF3mhCAzvnr+DcUiHgREfXE= -github.com/sashamelentyev/usestdlibvars v1.26.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= -github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.19.0 h1:gl5xMkOI0/E6Hxx0XCY2XujA3V7SNSefA8sC+3f1gnk= -github.com/securego/gosec/v2 v2.19.0/go.mod h1:hOkDcHz9J/XIgIlPDXalxjeVYsHxoWUc5zJSHxcB8YM= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 h1:rnO6Zp1YMQwv8AyxzuwsVohljJgp4L0ZqiCgtACsPsc= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9/go.mod h1:dg7lPlu/xK/Ut9SedURCoZbVCR4yC7fM65DtH9/CDHs= -github.com/securego/gosec/v2 v2.21.2 h1:deZp5zmYf3TWwU7A7cR2+SolbTpZ3HQiwFqnzQyEl3M= -github.com/securego/gosec/v2 v2.21.2/go.mod h1:au33kg78rNseF5PwPnTWhuYBFf534bvJRvOrgZ/bFzU= -github.com/securego/gosec/v2 v2.21.4 h1:Le8MSj0PDmOnHJgUATjD96PaXRvCpKC+DGJvwyy0Mlk= -github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyzzAhlr6lUTA= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= -github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= -github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= -github.com/sivchari/tenv v1.10.0 h1:g/hzMA+dBCKqGXgW8AV/1xIWhAvDrx0zFKNR48NFMg0= -github.com/sivchari/tenv v1.10.0/go.mod h1:tdY24masnVoZFxYrHv/nD6Tc8FbkEtAQEEziXpyMgqY= -github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= -github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= -github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= -github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= -github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= -github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= -github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= -github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= -github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= -github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= -github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.16 h1:4ChfhveiNLk4NveAZ9Pu2AN8QZ2nkUGFuadM9lrr5D0= -github.com/tetafro/godot v1.4.16/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/tetafro/godot v1.4.17 h1:pGzu+Ye7ZUEFx7LHU0dAKmCOXWsPjl7qA6iMGndsjPs= -github.com/tetafro/godot v1.4.17/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/tetafro/godot v1.4.18 h1:ouX3XGiziKDypbpXqShBfnNLTSjR8r3/HVzrtJ+bHlI= -github.com/tetafro/godot v1.4.18/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= -github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= -github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= -github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= -github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= -github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs= -github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= -github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= -github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= -github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= -github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= -github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= -github.com/ultraware/whitespace v0.1.0 h1:O1HKYoh0kIeqE8sFqZf1o0qbORXUCOQFrlaQyZsczZw= -github.com/ultraware/whitespace v0.1.0/go.mod h1:/se4r3beMFNmewJ4Xmz0nMQ941GJt+qmSHGP9emHYe0= -github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= -github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= -github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= -github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= -github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= -github.com/uudashr/iface v1.2.0 h1:ECJjh5q/1Zmnv/2yFpWV6H3oMg5+Mo+vL0aqw9Gjazo= -github.com/uudashr/iface v1.2.0/go.mod h1:Ux/7d/rAF3owK4m53cTVXL4YoVHKNqnoOeQHn2xrlp0= -github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= -github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= -github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= -github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= -github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= -github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= -github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= -github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= -github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= -github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4= -gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA= -gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= -gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= -go-simpler.org/musttag v0.9.0 h1:Dzt6/tyP9ONr5g9h9P3cnYWCxeBFRkd0uJL/w+1Mxos= -go-simpler.org/musttag v0.9.0/go.mod h1:gA9nThnalvNSKpEoyp3Ko4/vCX2xTpqKoUtNqXOnVR4= -go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= -go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= -go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= -go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= -go-simpler.org/sloglint v0.5.0 h1:2YCcd+YMuYpuqthCgubcF5lBSjb6berc5VMOYUHKrpY= -go-simpler.org/sloglint v0.5.0/go.mod h1:EUknX5s8iXqf18KQxKnaBHUPVriiPnOrPjjJcsaTcSQ= -go-simpler.org/sloglint v0.7.0 h1:rMZRxD9MbaGoRFobIOicMxZzum7AXNFDlez6xxJs5V4= -go-simpler.org/sloglint v0.7.0/go.mod h1:g9SXiSWY0JJh4LS39/Q0GxzP/QX2cVcbTOYhDpXrJEs= -go-simpler.org/sloglint v0.7.1 h1:qlGLiqHbN5islOxjeLXoPtUdZXb669RW+BDQ+xOSNoU= -go-simpler.org/sloglint v0.7.1/go.mod h1:OlaVDRh/FKKd4X4sIMbsz8st97vomydceL146Fthh/c= -go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= -go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= -honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -honnef.co/go/tools v0.5.0 h1:29uoiIormS3Z6R+t56STz/oI4v+mB51TSmEOdJPgRnE= -honnef.co/go/tools v0.5.0/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= -mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= -mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14 h1:zCr3iRRgdk5eIikZNDphGcM6KGVTx3Yu+/Uu9Es254w= -mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= -mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 h1:Nykk7fggxChwLK4rUPYESzeIwqsuxXXlFEAh5YhaMRo= -mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/.bingo/jb.mod b/.bingo/jb.mod deleted file mode 100644 index 732a8054017..00000000000 --- a/.bingo/jb.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.18 - -require github.com/jsonnet-bundler/jsonnet-bundler v0.5.1 // cmd/jb diff --git a/.bingo/jb.sum b/.bingo/jb.sum deleted file mode 100644 index 59c58ef41f1..00000000000 --- a/.bingo/jb.sum +++ /dev/null @@ -1,50 +0,0 @@ -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/jsonnet-bundler/jsonnet-bundler v0.5.1 h1:eUd6EA1Qzz73Q4NLNLOrNkMb96+6NTTERbX9lqaxVwk= -github.com/jsonnet-bundler/jsonnet-bundler v0.5.1/go.mod h1:Qrdw/7mOFS2SKCOALKFfEH8gdvXJi8XZjw9g5ilpf4I= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.bingo/lefthook.mod b/.bingo/lefthook.mod deleted file mode 100644 index 557c374fd91..00000000000 --- a/.bingo/lefthook.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.20 - -require github.com/evilmartians/lefthook v1.4.8 diff --git a/.bingo/lefthook.sum b/.bingo/lefthook.sum deleted file mode 100644 index 4f9fd11d4c6..00000000000 --- a/.bingo/lefthook.sum +++ /dev/null @@ -1,523 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= -github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= -github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evilmartians/lefthook v1.4.8 h1:8FmXWtfFiEZw3w18JbhVrp3g+Iy/j2XEo6gcC25+4KA= -github.com/evilmartians/lefthook v1.4.8/go.mod h1:anwwu2QiCEnsOCBHfRgGOB3/sd9FMVNhmY8l9DDQAG8= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= -github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= -github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM= -gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/.bingo/swagger.mod b/.bingo/swagger.mod deleted file mode 100644 index ef5b4b6e2c6..00000000000 --- a/.bingo/swagger.mod +++ /dev/null @@ -1,5 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.18 - -require github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 // cmd/swagger diff --git a/.bingo/swagger.sum b/.bingo/swagger.sum deleted file mode 100644 index 0dc152e6e87..00000000000 --- a/.bingo/swagger.sum +++ /dev/null @@ -1,810 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU= -github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= -github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= -github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= -github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= -github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= -github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= -github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68= -github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= -github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= -github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.21.1 h1:/KIG00BzA2x2HRStX2tnhbqbQdPcFlkgsYCiNY20FZs= -github.com/go-openapi/runtime v0.24.1 h1:Sml5cgQKGYQHF+M7yYSHaH1eOjvTykrddTE/KtQVjqo= -github.com/go-openapi/runtime v0.24.1/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= -github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= -github.com/go-openapi/strfmt v0.21.1 h1:G6s2t5V5kGCHLVbSdZ/6lI8Wm4OzoPFkc3/cjAsKQrM= -github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= -github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= -github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw= -github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= -github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= -github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= -github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/go-swagger/go-swagger v0.29.0 h1:z3YoZtLvS1Y8TE/PCat1VypcZxM0IgKLt0NvZxQyNl8= -github.com/go-swagger/go-swagger v0.29.0/go.mod h1:Z4GJzI+bHKKkGB2Ji1rawpi3/ldXX8CkzGIa9HAC5EE= -github.com/go-swagger/go-swagger v0.30.2 h1:23odPUyQZdkNFZZSBJ3mqYYcdh+LnuReEbdWN18OMRo= -github.com/go-swagger/go-swagger v0.30.2/go.mod h1:neDPes8r8PCz2JPvHRDj8BTULLh4VJUt7n6MpQqxhHM= -github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 h1:KFcZmKdZmapAog2+eL1buervAYrYolBZk7fMecPPDmo= -github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I= -github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02 h1:J6YiT/eg3gAfKMdVCkWXe6khsO+nxa8W4URZ4AUqzbA= -github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= -github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= -github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= -go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= -go.mongodb.org/mongo-driver v1.8.2 h1:8ssUXufb90ujcIvR6MyE1SchaNj0SFxsakiZgxIyrMk= -go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.10.1 h1:NujsPveKwHaWuKUer/ceo9DzEe7HIj1SlJ6uvXZG0S4= -go.mongodb.org/mongo-driver v1.10.1/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.3 h1:jRskFVxYaMGAMUbN0UZ7niA9gzL9B49DOqE78vg0k3w= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/.bingo/variables.env b/.bingo/variables.env index 03b44adfca5..b4e2c715f3e 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -8,19 +8,5 @@ if [ -z "$GOBIN" ]; then fi -BRA="${GOBIN}/bra-v0.0.0-20200517080246-1e3013ecaff8" - -COG="${GOBIN}/cog-v0.0.15" - -CUE="${GOBIN}/cue-v0.5.0" - DRONE="${GOBIN}/drone-v1.5.0" -GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.62.0" - -JB="${GOBIN}/jb-v0.5.1" - -LEFTHOOK="${GOBIN}/lefthook-v1.4.8" - -SWAGGER="${GOBIN}/swagger-v0.30.6-0.20240310114303-db51e79a0e37" - diff --git a/.citools/bra/go.mod b/.citools/bra/go.mod index 7562899364f..181d998f0df 100644 --- a/.citools/bra/go.mod +++ b/.citools/bra/go.mod @@ -5,7 +5,7 @@ go 1.24.1 tool github.com/unknwon/bra require ( - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -17,6 +17,6 @@ require ( github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect github.com/urfave/cli v1.22.16 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect ) diff --git a/.citools/bra/go.sum b/.citools/bra/go.sum index c96783ededd..4bb1c2dc09e 100644 --- a/.citools/bra/go.sum +++ b/.citools/bra/go.sum @@ -1,6 +1,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= @@ -55,8 +56,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/cog/go.mod b/.citools/cog/go.mod index 6c1ef45fb51..5abd47b7768 100644 --- a/.citools/cog/go.mod +++ b/.citools/cog/go.mod @@ -44,6 +44,7 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/.citools/cog/go.sum b/.citools/cog/go.sum index dc4e755d78a..d31c44b91b0 100644 --- a/.citools/cog/go.sum +++ b/.citools/cog/go.sum @@ -93,8 +93,8 @@ golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= diff --git a/.citools/cue/go.mod b/.citools/cue/go.mod index 0584e0a256e..8cb726b1501 100644 --- a/.citools/cue/go.mod +++ b/.citools/cue/go.mod @@ -29,7 +29,7 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/.citools/cue/go.sum b/.citools/cue/go.sum index c535a6ae5eb..1e3e963bb0d 100644 --- a/.citools/cue/go.sum +++ b/.citools/cue/go.sum @@ -61,8 +61,8 @@ golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= diff --git a/.citools/drone/go.mod b/.citools/drone/go.mod deleted file mode 100644 index 4c1fba053f9..00000000000 --- a/.citools/drone/go.mod +++ /dev/null @@ -1,77 +0,0 @@ -module drone - -go 1.24.1 - -tool github.com/drone/drone-cli/drone - -replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible - -require ( - github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/bmatcuk/doublestar v1.3.4 // indirect - github.com/buildkite/yaml v2.1.0+incompatible // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dchest/uniuri v1.2.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v27.5.1+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/drone-runners/drone-runner-docker v1.8.3 // indirect - github.com/drone/drone-cli v1.8.0 // indirect - github.com/drone/drone-go v1.7.1 // indirect - github.com/drone/envsubst v1.0.3 // indirect - github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f // indirect - github.com/drone/runner-go v1.12.0 // indirect - github.com/drone/signal v1.0.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-jsonnet v0.18.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/urfave/cli v1.22.16 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gotest.tools/v3 v3.5.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) diff --git a/.citools/drone/go.sum b/.citools/drone/go.sum deleted file mode 100644 index 4e6b0dba301..00000000000 --- a/.citools/drone/go.sum +++ /dev/null @@ -1,338 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= -github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= -github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= -github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8= -github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= -github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= -github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/drone-runners/drone-runner-docker v1.8.3 h1:uUnC45C1JMSLW+9uy6RoKG5ugzeXWN89pygs9BMLObY= -github.com/drone-runners/drone-runner-docker v1.8.3/go.mod h1:JR3pZeVZKKpkbTajiq0YtAx9WutkODdVKZGNR83kEwE= -github.com/drone/drone-cli v1.8.0 h1:tpp+GPonS87IKMZCGbIoa+zfDwiuJDL3NIC6S7neNrU= -github.com/drone/drone-cli v1.8.0/go.mod h1:zu6/7OpQjWBw/5VG0M3K4iJc6kSoTrjnY7CRLBrGH84= -github.com/drone/drone-go v1.7.1 h1:ZX+3Rs8YHUSUQ5mkuMLmm1zr1ttiiE2YGNxF3AnyDKw= -github.com/drone/drone-go v1.7.1/go.mod h1:fxCf9jAnXDZV1yDr0ckTuWd1intvcQwfJmTRpTZ1mXg= -github.com/drone/envsubst v1.0.2/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0= -github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= -github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= -github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU= -github.com/drone/runner-go v1.12.0 h1:zUjDj9ylsJ4n4Mvy4znddq/Z4EBzcUXzTltpzokKtgs= -github.com/drone/runner-go v1.12.0/go.mod h1:vu4pPPYDoeN6vdYQAY01GGGsAIW4aLganJNaa8Fx8zE= -github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= -github.com/drone/signal v1.0.0/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= -github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/gogo/protobuf v0.0.0-20170307180453-100ba4e88506/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= -github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= -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/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby v27.5.1+incompatible h1:/pN59F/t3U7Q4FPzV88nzqf7Fp0qqCSL2KzhZaiKcKw= -github.com/moby/moby v27.5.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= -github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4/go.mod h1:cojhOHk1gbMeklOyDP2oKKLftefXoJreOQGOrXk+Z38= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= -github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/.citools/golangci-lint/go.mod b/.citools/golangci-lint/go.mod index 93df12ec8c3..e02feef5f9d 100644 --- a/.citools/golangci-lint/go.mod +++ b/.citools/golangci-lint/go.mod @@ -34,19 +34,19 @@ require ( github.com/butuzov/mirror v1.2.0 // indirect github.com/catenacyber/perfsprint v0.7.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.2 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.2.1 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/daixiang0/gci v0.13.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.8 // indirect github.com/go-critic/go-critic v0.11.5 // indirect @@ -61,7 +61,6 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.12.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect github.com/golangci/go-printf-func-name v0.1.0 // indirect github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect @@ -71,7 +70,8 @@ require ( github.com/golangci/plugin-module-register v0.1.1 // indirect github.com/golangci/revgrep v0.5.3 // indirect github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect @@ -88,6 +88,7 @@ require ( github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect github.com/kisielk/errcheck v1.8.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect @@ -96,31 +97,33 @@ require ( github.com/ldez/tagliatelle v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/macabu/inamedparam v0.1.3 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/maratori/testableexamples v1.0.0 // indirect github.com/maratori/testpackage v1.1.1 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect + github.com/matryer/is v1.4.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgechev/revive v1.5.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/moricho/tparallel v0.3.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.18.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect + github.com/onsi/ginkgo/v2 v2.22.0 // indirect + github.com/onsi/gomega v1.36.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.6.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/client_golang v1.21.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect @@ -131,6 +134,8 @@ require ( github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect @@ -141,18 +146,18 @@ require ( github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/tenv v1.12.1 // indirect github.com/sonatard/noctx v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.12.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.19.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.18 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect @@ -170,18 +175,18 @@ require ( gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.13.0 // indirect go-simpler.org/sloglint v0.7.2 // indirect - go.uber.org/atomic v1.7.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.27.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/.citools/golangci-lint/go.sum b/.citools/golangci-lint/go.sum index c8205a75d73..a9f2d2abfb2 100644 --- a/.citools/golangci-lint/go.sum +++ b/.citools/golangci-lint/go.sum @@ -2,39 +2,6 @@ 4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= 4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= 4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= @@ -45,10 +12,8 @@ github.com/Antonboom/nilnil v1.0.0 h1:n+v+B12dsE5tbAqRODXmEKfZv9j2KcTBrp+LkoM4HZ github.com/Antonboom/nilnil v1.0.0/go.mod h1:fDJ1FSFoLN6yoG65ANb1WihItf6qt9PJVTn/s2IrcII= github.com/Antonboom/testifylint v1.5.0 h1:dlUIsDMtCrZWUnvkaCz3quJCoIjaGi41GzjPBGkkJ8A= github.com/Antonboom/testifylint v1.5.0/go.mod h1:wqaJbu0Blb5Wag2wv7Z5xt+CIV+eVLxtGZrlK13z3AE= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= @@ -65,11 +30,6 @@ github.com/alecthomas/go-check-sumtype v0.2.0 h1:Bo+e4DFf3rs7ME9w/0SU/g6nmzJaphd github.com/alecthomas/go-check-sumtype v0.2.0/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= @@ -80,10 +40,6 @@ github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8ger github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= @@ -104,35 +60,25 @@ github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyy github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.2.1 h1:M07spnNEQoALOJhwrImSrJLaxwuiQK+hA2DeajBlwYk= github.com/ckaznocha/intrange v0.2.1/go.mod h1:7NEhVyf8fzZO5Ds7CRaqPEm52Ut83hsTiL5zbER/HYk= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -141,30 +87,20 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghostiam/protogetter v0.3.8 h1:LYcXbYvybUyTIxN2Mj9h6rHrDZBDwZloPoKctWrFyJY= github.com/ghostiam/protogetter v0.3.8/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/go-critic/go-critic v0.11.5 h1:TkDTOn5v7EEngMxu8KbuFqFR43USaaH8XRJLz1jhVYA= github.com/go-critic/go-critic v0.11.5/go.mod h1:wu6U7ny9PiaHaZHcvMDmdysMqvDem162Rh3zWTrqk8M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= @@ -194,36 +130,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= @@ -242,37 +148,15 @@ github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAz github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +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/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -290,13 +174,10 @@ github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Rep github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= @@ -305,32 +186,18 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= @@ -349,16 +216,17 @@ github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84Yrj github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -367,23 +235,16 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgechev/revive v1.5.0 h1:oaSmjA7rP8+HyoRuCgC531VHwnLH1AlJdjj+1AnQceQ= github.com/mgechev/revive v1.5.0/go.mod h1:L6T3H8EoerRO86c7WuGpvohIUmiploGiyoYbtIWFmV8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= @@ -394,10 +255,10 @@ github.com/nunnatsa/ginkgolinter v0.18.0 h1:ZXO1wKhPg3A6LpbN5dMuqwhfOjN5c3ous8Yd github.com/nunnatsa/ginkgolinter v0.18.0/go.mod h1:vPrWafSULmjMGCMsfGA908if95VnHQNAahvSBOjTuWs= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -405,42 +266,23 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= @@ -456,7 +298,6 @@ github.com/raeperd/recvcheck v0.1.2/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtz github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -464,6 +305,10 @@ github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= @@ -478,9 +323,6 @@ github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqP github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= @@ -489,31 +331,30 @@ github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -521,10 +362,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= @@ -558,7 +399,6 @@ github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+ github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -572,63 +412,25 @@ go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k= golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -639,100 +441,36 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -740,12 +478,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -755,17 +490,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -773,57 +506,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= @@ -837,121 +529,28 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/.citools/jb/go.mod b/.citools/jb/go.mod index a8df6197a17..562db7944c6 100644 --- a/.citools/jb/go.mod +++ b/.citools/jb/go.mod @@ -8,13 +8,13 @@ require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/jsonnet-bundler/jsonnet-bundler v0.5.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.10.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect ) diff --git a/.citools/jb/go.sum b/.citools/jb/go.sum index fa93a05ce6a..cab9399fdd7 100644 --- a/.citools/jb/go.sum +++ b/.citools/jb/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/jsonnet-bundler/jsonnet-bundler v0.5.1 h1:eUd6EA1Qzz73Q4NLNLOrNkMb96+6NTTERbX9lqaxVwk= github.com/jsonnet-bundler/jsonnet-bundler v0.5.1/go.mod h1:Qrdw/7mOFS2SKCOALKFfEH8gdvXJi8XZjw9g5ilpf4I= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -56,8 +56,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/lefthook/go.mod b/.citools/lefthook/go.mod index 2fbfd58e665..12bbe517a1d 100644 --- a/.citools/lefthook/go.mod +++ b/.citools/lefthook/go.mod @@ -11,7 +11,7 @@ require ( github.com/charmbracelet/lipgloss v0.6.0 // indirect github.com/creack/pty v1.1.18 // indirect github.com/evilmartians/lefthook v1.4.8 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -40,7 +40,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect diff --git a/.citools/lefthook/go.sum b/.citools/lefthook/go.sum index 1b0fbdb81ce..6cc7e14be05 100644 --- a/.citools/lefthook/go.sum +++ b/.citools/lefthook/go.sum @@ -16,8 +16,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/evilmartians/lefthook v1.4.8 h1:8FmXWtfFiEZw3w18JbhVrp3g+Iy/j2XEo6gcC25+4KA= github.com/evilmartians/lefthook v1.4.8/go.mod h1:anwwu2QiCEnsOCBHfRgGOB3/sd9FMVNhmY8l9DDQAG8= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -98,8 +98,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= diff --git a/.citools/swagger/go.mod b/.citools/swagger/go.mod new file mode 100644 index 00000000000..b392184630c --- /dev/null +++ b/.citools/swagger/go.mod @@ -0,0 +1,67 @@ +module swagger + +go 1.24.1 + +tool github.com/go-swagger/go-swagger/cmd/swagger + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/inflect v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/runtime v0.28.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/toqueteos/webbrowser v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.16.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/.citools/swagger/go.sum b/.citools/swagger/go.sum new file mode 100644 index 00000000000..2f34b403322 --- /dev/null +++ b/.citools/swagger/go.sum @@ -0,0 +1,132 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= +github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 h1:KFcZmKdZmapAog2+eL1buervAYrYolBZk7fMecPPDmo= +github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I= +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/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/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= +go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a432ee06073..f55638960d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -69,6 +69,7 @@ /pkg/apis/provisioning @grafana/grafana-git-ui-sync-team /public/app/features/provisioning @grafana/grafana-git-ui-sync-team +/conf/provisioning/sample @grafana/grafana-git-ui-sync-team /pkg/registry/apis/provisioning @grafana/grafana-git-ui-sync-team /apps/alerting/ @grafana/alerting-backend @@ -126,6 +127,7 @@ /pkg/apimachinery/errutil/ @grafana/grafana-backend-group /pkg/promlib @grafana/oss-big-tent /pkg/storage/ @grafana/grafana-search-and-storage +/pkg/storage/secret/ @grafana/grafana-operator-experience-squad /pkg/services/annotations/ @grafana/grafana-search-and-storage /pkg/services/apikey/ @grafana/identity-squad /pkg/services/cleanup/ @grafana/grafana-backend-group @@ -316,6 +318,7 @@ /Makefile @grafana/grafana-developer-enablement-squad /scripts/build/ @grafana/grafana-developer-enablement-squad /scripts/list-release-artifacts.sh @grafana/grafana-developer-enablement-squad +/scripts/releasefinder.sh @baldm0mma /.trivyignore @grafana/grafana-backend-services-squad # OSS Plugin Partnerships backend code @@ -736,6 +739,7 @@ embed.go @grafana/grafana-as-code /pkg/registry/apis/ @grafana/grafana-app-platform-squad /pkg/registry/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend /pkg/registry/apis/query @grafana/grafana-datasources-core-services +/pkg/registry/apis/secret @grafana/grafana-operator-experience-squad /pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend /pkg/registry/apps/advisor @grafana/plugins-platform-backend /pkg/codegen/ @grafana/grafana-as-code @@ -765,6 +769,8 @@ embed.go @grafana/grafana-as-code /.github/workflows/alerting-swagger-gen.yml @grafana/alerting-backend /.github/workflows/alerting-update-module.yml @grafana/alerting-backend /.github/workflows/auto-milestone.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/backend-code-checks.yml @grafana/grafana-backend-group +/.github/workflows/backend-unit-tests.yml @grafana/grafana-backend-group /.github/workflows/backport.yml @grafana/grafana-developer-enablement-squad /.github/workflows/bump-version.yml @grafana/grafana-developer-enablement-squad /.github/workflows/close-milestone.yml @grafana/grafana-developer-enablement-squad @@ -784,18 +790,16 @@ embed.go @grafana/grafana-as-code /.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling /.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad /.github/workflows/issue-opened.yml @grafana/grafana-community-support +/.github/workflows/lint-build-docs.yml @grafana/docs-tooling /.github/workflows/metrics-collector.yml @torkelo /.github/workflows/milestone.yml @tolzhabayev -/.github/workflows/pr-backend-code-checks.yml @grafana/grafana-backend-group /.github/workflows/pr-checks.yml @tolzhabayev /.github/workflows/pr-codeql-analysis-go.yml @DanCech /.github/workflows/pr-codeql-analysis-javascript.yml @DanCech /.github/workflows/pr-codeql-analysis-python.yml @DanCech /.github/workflows/pr-commands.yml @tolzhabayev -/.github/workflows/pr-lint-build-docs.yml @grafana/docs-tooling /.github/workflows/pr-patch-check.yml @grafana/grafana-developer-enablement-squad /.github/workflows/pr-test-integration.yml @grafana/grafana-backend-group -/.github/workflows/pr-backend-unit-tests.yml @grafana/grafana-backend-group /.github/workflows/pr-backend-coverage.yml @grafana/grafana-backend-group /.github/workflows/sync-mirror.yml @grafana/grafana-developer-enablement-squad /.github/workflows/publish-technical-documentation-next.yml @grafana/docs-tooling diff --git a/.github/workflows/pr-backend-code-checks.yml b/.github/workflows/backend-code-checks.yml similarity index 86% rename from .github/workflows/pr-backend-code-checks.yml rename to .github/workflows/backend-code-checks.yml index f9b0b6eeecc..75249775bad 100644 --- a/.github/workflows/pr-backend-code-checks.yml +++ b/.github/workflows/backend-code-checks.yml @@ -7,6 +7,13 @@ on: - '*.md' - 'docs/**' - 'latest.json' + push: + branches: + - main + paths-ignore: + - '*.md' + - 'docs/**' + - 'latest.json' permissions: contents: read @@ -44,8 +51,8 @@ jobs: - name: Generate and Validate OpenAPI Specs run: | - # For forks, we'll just run the basic swagger-gen without validation - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + # For PRs from forks, we'll just run the basic swagger-gen without validation + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then echo "PR is from a fork, skipping enterprise-based validation" make swagger-gen exit 0 @@ -54,7 +61,7 @@ jobs: # Clean and regenerate OpenAPI specs make swagger-clean && make openapi3-gen - # Check if the generated specs differ from what's in the PR + # Check if the generated specs differ from what's in the repository for f in public/api-merged.json public/openapi3.json; do git add $f; done if [ -z "$(git diff --name-only --cached)" ]; then echo "OpenAPI specs are up to date!" diff --git a/.github/workflows/pr-backend-unit-tests.yml b/.github/workflows/backend-unit-tests.yml similarity index 96% rename from .github/workflows/pr-backend-unit-tests.yml rename to .github/workflows/backend-unit-tests.yml index f81b9c8f6ac..e9412ca9a67 100644 --- a/.github/workflows/pr-backend-unit-tests.yml +++ b/.github/workflows/backend-unit-tests.yml @@ -5,6 +5,13 @@ on: paths-ignore: - 'docs/**' - '**/*.md' + push: + branches: + - main + - release-*.*.* + paths-ignore: + - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/pr-lint-build-docs.yml b/.github/workflows/lint-build-docs.yml similarity index 92% rename from .github/workflows/pr-lint-build-docs.yml rename to .github/workflows/lint-build-docs.yml index 8ed36d3da9a..ecd6151e228 100644 --- a/.github/workflows/pr-lint-build-docs.yml +++ b/.github/workflows/lint-build-docs.yml @@ -7,6 +7,14 @@ on: - 'docs/**' - 'packages/**/*.md' - 'latest.json' + push: + branches: + - main + paths: + - '*.md' + - 'docs/**' + - 'packages/**/*.md' + - 'latest.json' jobs: docs: diff --git a/.github/workflows/pr-frontend-unit-tests.yml b/.github/workflows/pr-frontend-unit-tests.yml index e6a71ea7871..3cbdf944b61 100644 --- a/.github/workflows/pr-frontend-unit-tests.yml +++ b/.github/workflows/pr-frontend-unit-tests.yml @@ -4,8 +4,7 @@ on: push: branches: - main -# Commenting out to unblock backports to release branches -# - release-*.*.* + - release-*.*.* jobs: frontend-unit-tests: diff --git a/.github/workflows/pr-k8s-codegen-check.yml b/.github/workflows/pr-k8s-codegen-check.yml index 71dd29b6354..ae5b6d3893a 100644 --- a/.github/workflows/pr-k8s-codegen-check.yml +++ b/.github/workflows/pr-k8s-codegen-check.yml @@ -9,6 +9,7 @@ on: - "pkg/aggregator/apis/**" - "pkg/apimachinery/apis/**" - "hack/**" + - "apps/**" - "*.sum" jobs: diff --git a/.github/workflows/storybook-verification.yml b/.github/workflows/storybook-verification.yml index 72eb6a4ad4c..efab7af6c4c 100644 --- a/.github/workflows/storybook-verification.yml +++ b/.github/workflows/storybook-verification.yml @@ -4,7 +4,13 @@ on: pull_request: paths: - 'packages/grafana-ui/**' - - '.github/workflows/storybook-verification.yml' + - '!docs/**' + - '!*.md' + push: + branches: + - main + paths: + - 'packages/grafana-ui/**' - '!docs/**' - '!*.md' diff --git a/.gitignore b/.gitignore index eabffd1dbf4..8858c9205f4 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,9 @@ conf/custom.ini /conf/provisioning/**/*.yaml !/conf/provisioning/**/sample.yaml +# sample yaml files +!/conf/provisioning/sample/*.yaml + /conf/ldap_dev.toml /conf/ldap_freeipa.toml profile.cov diff --git a/.prettierignore b/.prettierignore index 23d9231bc49..b9f59a37afb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,7 @@ .git .github .yarn +.bingo build compiled data diff --git a/Dockerfile b/Dockerfile index 08360d8b06c..bb334ff8dfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,8 +65,8 @@ COPY .citools/cue .citools/cue COPY .citools/cog .citools/cog COPY .citools/lefthook .citools/lefthook COPY .citools/jb .citools/jb -COPY .citools/drone .citools/drone COPY .citools/golangci-lint .citools/golangci-lint +COPY .citools/swagger ./citools/swagger # Include vendored dependencies COPY pkg/util/xorm pkg/util/xorm diff --git a/Makefile b/Makefile index 8b0c6664c18..9f022f833b2 100644 --- a/Makefile +++ b/Makefile @@ -60,15 +60,15 @@ NGALERT_SPEC_TARGET = pkg/services/ngalert/api/tooling/api.json $(NGALERT_SPEC_TARGET): +$(MAKE) -C pkg/services/ngalert/api/tooling api.json -$(MERGED_SPEC_TARGET): swagger-oss-gen swagger-enterprise-gen $(NGALERT_SPEC_TARGET) $(SWAGGER) ## Merge generated and ngalert API specs +$(MERGED_SPEC_TARGET): swagger-oss-gen swagger-enterprise-gen $(NGALERT_SPEC_TARGET) ## Merge generated and ngalert API specs # known conflicts DsPermissionType, AddApiKeyCommand, Json, Duration (identical models referenced by both specs) - $(SWAGGER) mixin -q $(SPEC_TARGET) $(ENTERPRISE_SPEC_TARGET) $(NGALERT_SPEC_TARGET) --ignore-conflicts -o $(MERGED_SPEC_TARGET) + GODEBUG=gotypesalias=0 $(GO) tool swagger mixin -q $(SPEC_TARGET) $(ENTERPRISE_SPEC_TARGET) $(NGALERT_SPEC_TARGET) --ignore-conflicts -o $(MERGED_SPEC_TARGET) .PHONY: swagger-oss-gen -swagger-oss-gen: $(SWAGGER) ## Generate API Swagger specification +swagger-oss-gen: ## Generate API Swagger specification @echo "re-generating swagger for OSS" rm -f $(SPEC_TARGET) - SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -q -m -w pkg/server -o $(SPEC_TARGET) \ + SWAGGER_GENERATE_EXTENSION=false GODEBUG=gotypesalias=0 $(GO) tool swagger generate spec -q -m -w pkg/server -o $(SPEC_TARGET) \ -x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ -x "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" \ -x "github.com/prometheus/alertmanager" \ @@ -83,10 +83,10 @@ ifeq ("$(wildcard $(ENTERPRISE_EXT_FILE))","") ## if enterprise is not enabled swagger-enterprise-gen: @echo "skipping re-generating swagger for enterprise: not enabled" else -swagger-enterprise-gen: $(SWAGGER) ## Generate API Swagger specification +swagger-enterprise-gen: ## Generate API Swagger specification @echo "re-generating swagger for enterprise" rm -f $(ENTERPRISE_SPEC_TARGET) - SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -q -m -w pkg/server -o $(ENTERPRISE_SPEC_TARGET) \ + SWAGGER_GENERATE_EXTENSION=false GODEBUG=gotypesalias=0 $(GO) tool swagger generate spec -q -m -w pkg/server -o $(ENTERPRISE_SPEC_TARGET) \ -x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ -x "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" \ -x "github.com/prometheus/alertmanager" \ @@ -99,8 +99,8 @@ endif swagger-gen: gen-go $(MERGED_SPEC_TARGET) swagger-validate .PHONY: swagger-validate -swagger-validate: $(MERGED_SPEC_TARGET) $(SWAGGER) ## Validate API spec - $(SWAGGER) validate --skip-warnings $(<) +swagger-validate: $(MERGED_SPEC_TARGET) # Validate API spec + GODEBUG=gotypesalias=0 $(GO) tool swagger validate --skip-warnings $(<) .PHONY: swagger-clean swagger-clean: @@ -311,6 +311,13 @@ test-go-integration-memcached: ## Run integration tests for memcached cache. $(GO) clean -testcache MEMCACHED_HOSTS=localhost:11211 $(GO) test $(GO_RACE_FLAG) $(GO_TEST_FLAGS) -run IntegrationMemcached -covermode=atomic -timeout=2m $(GO_INTEGRATION_TESTS) +.PHONY: test-go-integration-spanner +test-go-integration-spanner: ## Run integration tests for Spanner backend with flags. Uses spanner-emulator on localhost:9010 and localhost:9020. + @if [ "${WIRE_TAGS}" != "enterprise" ]; then echo "Spanner integration test require enterprise setup"; exit 1; fi + @echo "test backend integration spanner tests" + GRAFANA_TEST_DB=spanner SPANNER_DB=emulator \ + $(GO) test $(GO_RACE_FLAG) $(GO_TEST_FLAGS) -p=1 -count=1 -v -run "^TestIntegration" -covermode=atomic -timeout=2m $(GO_INTEGRATION_TESTS) + .PHONY: test-js test-js: ## Run tests for frontend. @echo "test frontend" @@ -434,7 +441,6 @@ protobuf: ## Compile protobuf definitions go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 buf generate pkg/plugins/backendplugin/pluginextensionv2 --template pkg/plugins/backendplugin/pluginextensionv2/buf.gen.yaml - buf generate pkg/plugins/backendplugin/secretsmanagerplugin --template pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml buf generate pkg/apis/secret/v0alpha1/decrypt --template pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml buf generate pkg/storage/unified/resource --template pkg/storage/unified/resource/buf.gen.yaml buf generate pkg/services/authz/proto/v1 --template pkg/services/authz/proto/v1/buf.gen.yaml diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go index 2bcb8d43f19..cdca40cbd77 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go @@ -14,6 +14,7 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/infra/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" ) @@ -30,6 +31,7 @@ type Runner struct { evaluationInterval time.Duration maxHistory int namespace string + log log.Logger } // NewRunner creates a new Runner. @@ -66,22 +68,25 @@ func New(cfg app.Config) (app.Runnable, error) { evaluationInterval: evalInterval, maxHistory: maxHistory, namespace: namespace, + log: log.New("advisor.checkscheduler"), }, nil } func (r *Runner) Run(ctx context.Context) error { lastCreated, err := r.checkLastCreated(ctx) if err != nil { - return err - } - - // do an initial creation if necessary - if lastCreated.IsZero() { - err = r.createChecks(ctx) - if err != nil { - klog.Error("Error creating new check reports", "error", err) - } else { - lastCreated = time.Now() + r.log.Error("Error getting last check creation time", "error", err) + // Wait for interval to create the next scheduled check + lastCreated = time.Now() + } else { + // do an initial creation if necessary + if lastCreated.IsZero() { + err = r.createChecks(ctx) + if err != nil { + klog.Error("Error creating new check reports", "error", err) + } else { + lastCreated = time.Now() + } } } diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go index 4fd5876b9ff..23ce113df4a 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go @@ -11,28 +11,35 @@ import ( "github.com/grafana/grafana-app-sdk/resource" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestRunner_Run_ErrorOnList(t *testing.T) { - mockCheckService := &MockCheckService{} - mockClient := &MockClient{ - listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { - return nil, errors.New("list error") - }, - createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { - return &advisorv0alpha1.Check{}, nil - }, - } +func TestRunner_Run(t *testing.T) { + t.Run("does not crash when error on list", func(t *testing.T) { + mockCheckService := &MockCheckService{} + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return nil, errors.New("list error") + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return &advisorv0alpha1.Check{}, nil + }, + } - runner := &Runner{ - checkRegistry: mockCheckService, - client: mockClient, - } + runner := &Runner{ + checkRegistry: mockCheckService, + client: mockClient, + log: log.NewNopLogger(), + evaluationInterval: 1 * time.Hour, + } - err := runner.Run(context.Background()) - assert.Error(t, err) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := runner.Run(ctx) + assert.ErrorAs(t, err, &context.Canceled) + }) } func TestRunner_checkLastCreated_ErrorOnList(t *testing.T) { @@ -44,6 +51,7 @@ func TestRunner_checkLastCreated_ErrorOnList(t *testing.T) { runner := &Runner{ client: mockClient, + log: log.NewNopLogger(), } lastCreated, err := runner.checkLastCreated(context.Background()) @@ -68,6 +76,7 @@ func TestRunner_createChecks_ErrorOnCreate(t *testing.T) { runner := &Runner{ checkRegistry: mockCheckService, client: mockClient, + log: log.NewNopLogger(), } err := runner.createChecks(context.Background()) @@ -91,6 +100,7 @@ func TestRunner_createChecks_Success(t *testing.T) { runner := &Runner{ checkRegistry: mockCheckService, client: mockClient, + log: log.NewNopLogger(), } err := runner.createChecks(context.Background()) @@ -106,6 +116,7 @@ func TestRunner_cleanupChecks_ErrorOnList(t *testing.T) { runner := &Runner{ client: mockClient, + log: log.NewNopLogger(), } err := runner.cleanupChecks(context.Background()) @@ -126,6 +137,7 @@ func TestRunner_cleanupChecks_WithinMax(t *testing.T) { runner := &Runner{ client: mockClient, + log: log.NewNopLogger(), } err := runner.cleanupChecks(context.Background()) @@ -155,6 +167,7 @@ func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) { runner := &Runner{ client: mockClient, maxHistory: defaultMaxHistory, + log: log.NewNopLogger(), } err := runner.cleanupChecks(context.Background()) assert.ErrorContains(t, err, "delete error") @@ -190,6 +203,7 @@ func TestRunner_cleanupChecks_Success(t *testing.T) { runner := &Runner{ client: mockClient, maxHistory: defaultMaxHistory, + log: log.NewNopLogger(), } err := runner.cleanupChecks(context.Background()) assert.NoError(t, err) diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go index 2ed38e51ecb..3e563b3f88f 100644 --- a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go @@ -3,6 +3,7 @@ package checktyperegisterer import ( "context" "fmt" + "time" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" @@ -10,6 +11,7 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/infra/log" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -21,6 +23,9 @@ type Runner struct { checkRegistry checkregistry.CheckService client resource.Client namespace string + log log.Logger + retryAttempts int + retryDelay time.Duration } // NewRunner creates a new Runner. @@ -47,9 +52,32 @@ func New(cfg app.Config) (app.Runnable, error) { checkRegistry: checkRegistry, client: client, namespace: namespace, + log: log.New("advisor.checktyperegisterer"), + retryAttempts: 3, + retryDelay: time.Second * 5, }, nil } +func (r *Runner) createOrUpdate(ctx context.Context, obj resource.Object) error { + id := obj.GetStaticMetadata().Identifier() + _, err := r.client.Create(ctx, id, obj, resource.CreateOptions{}) + if err != nil { + if errors.IsAlreadyExists(err) { + // Already exists, update + r.log.Debug("Check type already exists, updating", "identifier", id) + _, err = r.client.Update(ctx, id, obj, resource.UpdateOptions{}) + if err != nil { + // Ignore the error, it's probably due to a race condition + r.log.Error("Error updating check type", "error", err) + } + return nil + } + return err + } + r.log.Debug("Check type registered successfully", "identifier", id) + return nil +} + func (r *Runner) Run(ctx context.Context) error { for _, t := range r.checkRegistry.Checks() { steps := t.Steps() @@ -72,19 +100,19 @@ func (r *Runner) Run(ctx context.Context) error { Steps: stepTypes, }, } - id := obj.GetStaticMetadata().Identifier() - _, err := r.client.Create(ctx, id, obj, resource.CreateOptions{}) - if err != nil { - if errors.IsAlreadyExists(err) { - // Already exists, update - _, err = r.client.Update(ctx, id, obj, resource.UpdateOptions{}) - if err != nil { - return err + for i := 0; i < r.retryAttempts; i++ { + err := r.createOrUpdate(ctx, obj) + if err != nil { + r.log.Error("Error creating check type, retrying", "error", err, "attempt", i+1) + if i == r.retryAttempts-1 { + r.log.Error("Unable to register check type") } else { - continue + time.Sleep(r.retryDelay) } + continue } - return err + r.log.Debug("Check type registered successfully", "check_type", t.ID()) + break } } return nil diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go index 97fc7742504..4d4bab9adf3 100644 --- a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana-app-sdk/resource" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/infra/log" k8sErrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -117,7 +118,10 @@ func TestCheckTypesRegisterer_Run(t *testing.T) { createFunc: tt.createFunc, updateFunc: tt.updateFunc, }, - namespace: "custom-namespace", + namespace: "custom-namespace", + log: log.New("test"), + retryAttempts: 1, + retryDelay: 0, } err := r.Run(context.Background()) if err != nil { diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 8ba8c1f78e1..fec89a07666 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -36,19 +36,21 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 // indirect + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.22.0 // indirect + github.com/onsi/gomega v1.36.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.21.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -76,13 +78,13 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index a42cf3aacb2..1c690370732 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -44,8 +44,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -69,8 +69,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 h1:B7kt2We4CVCWRJGgaIyx8lhZqTeDAk6bspOkGYWoB/I= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= @@ -91,8 +91,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -231,8 +231,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -256,13 +256,13 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 8c2285debc3..1e5d5ccaf30 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -4,7 +4,7 @@ go 1.23.7 require ( github.com/grafana/grafana-app-sdk v0.31.0 - github.com/grafana/grafana-plugin-sdk-go v0.272.0 + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.1 @@ -12,8 +12,8 @@ require ( ) require ( - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -24,7 +24,7 @@ require ( 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.11.0 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.129.0 // indirect @@ -33,10 +33,10 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -54,8 +54,8 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect @@ -71,7 +71,7 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -90,7 +90,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect @@ -103,15 +103,15 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 33cfb1d9e90..5c384a5bcb7 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -1,10 +1,11 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -34,8 +35,8 @@ github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVo github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -59,16 +60,16 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -89,8 +90,8 @@ github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDR github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME= github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -124,10 +125,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -177,8 +178,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -251,8 +252,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= @@ -310,8 +311,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -335,12 +336,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 26ca2d35eec..2396c0cc1aa 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -554,7 +554,7 @@ RowsLayoutRowSpec: { collapsed: bool conditionalRendering?: ConditionalRenderingGroupKind repeat?: RowRepeatOptions - layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind + layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind | RowsLayoutKind } ResponsiveGridLayoutKind: { @@ -595,7 +595,7 @@ TabsLayoutTabKind: { TabsLayoutTabSpec: { title?: string - layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind } PanelSpec: { diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go index 5e5643192a6..f37ced365cf 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go @@ -219,6 +219,7 @@ func (o *Dashboard) Copy() resource.Object { } func (o *Dashboard) DeepCopyObject() runtime.Object { + // This really should be deep copy. If the generator tries to change it to o.Copy() it needs to be manually changed back to o.DeepCopy() and this comment added back. return o.DeepCopy() } diff --git a/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go index 8d45c64fc1a..137d7149477 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go @@ -219,6 +219,7 @@ func (o *Dashboard) Copy() resource.Object { } func (o *Dashboard) DeepCopyObject() runtime.Object { + // This really should be deep copy. If the generator tries to change it to o.Copy() it needs to be manually changed back to o.DeepCopy() and this comment added back. return o.DeepCopy() } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go index 5b59fbd93aa..4437f066574 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go @@ -219,6 +219,7 @@ func (o *Dashboard) Copy() resource.Object { } func (o *Dashboard) DeepCopyObject() runtime.Object { + // This really should be deep copy. If the generator tries to change it to o.Copy() it needs to be manually changed back to o.DeepCopy() and this comment added back. return o.DeepCopy() } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index b491bd8510d..801cd807bd3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -836,17 +836,17 @@ func NewDashboardRowsLayoutRowKind() *DashboardRowsLayoutRowKind { // +k8s:openapi-gen=true type DashboardRowsLayoutRowSpec struct { - Title *string `json:"title,omitempty"` - Collapsed bool `json:"collapsed"` - ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` - Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"` - Layout DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind `json:"layout"` + Title *string `json:"title,omitempty"` + Collapsed bool `json:"collapsed"` + ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` + Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"` + Layout DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind `json:"layout"` } // NewDashboardRowsLayoutRowSpec creates a new DashboardRowsLayoutRowSpec object. func NewDashboardRowsLayoutRowSpec() *DashboardRowsLayoutRowSpec { return &DashboardRowsLayoutRowSpec{ - Layout: *NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind(), + Layout: *NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(), } } @@ -1054,14 +1054,14 @@ func NewDashboardTabsLayoutTabKind() *DashboardTabsLayoutTabKind { // +k8s:openapi-gen=true type DashboardTabsLayoutTabSpec struct { - Title *string `json:"title,omitempty"` - Layout DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind `json:"layout"` + Title *string `json:"title,omitempty"` + Layout DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind `json:"layout"` } // NewDashboardTabsLayoutTabSpec creates a new DashboardTabsLayoutTabSpec object. func NewDashboardTabsLayoutTabSpec() *DashboardTabsLayoutTabSpec { return &DashboardTabsLayoutTabSpec{ - Layout: *NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind(), + Layout: *NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind(), } } @@ -2012,19 +2012,20 @@ func (resource *DashboardGridLayoutItemKindOrGridLayoutRowKind) UnmarshalJSON(ra } // +k8s:openapi-gen=true -type DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind struct { +type DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind struct { GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"` ResponsiveGridLayoutKind *DashboardResponsiveGridLayoutKind `json:"ResponsiveGridLayoutKind,omitempty"` TabsLayoutKind *DashboardTabsLayoutKind `json:"TabsLayoutKind,omitempty"` + RowsLayoutKind *DashboardRowsLayoutKind `json:"RowsLayoutKind,omitempty"` } -// NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind creates a new DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind object. -func NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind() *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind { - return &DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind{} +// NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind creates a new DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind object. +func NewDashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind() *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind { + return &DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{} } -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` as JSON. -func (resource DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) MarshalJSON() ([]byte, error) { +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind` as JSON. +func (resource DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind) MarshalJSON() ([]byte, error) { if resource.GridLayoutKind != nil { return json.Marshal(resource.GridLayoutKind) } @@ -2034,11 +2035,14 @@ func (resource DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind if resource.TabsLayoutKind != nil { return json.Marshal(resource.TabsLayoutKind) } + if resource.RowsLayoutKind != nil { + return json.Marshal(resource.RowsLayoutKind) + } return []byte("null"), nil } -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` from JSON. -func (resource *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) UnmarshalJSON(raw []byte) error { +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind` from JSON. +func (resource *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind) UnmarshalJSON(raw []byte) error { if raw == nil { return nil } @@ -2071,6 +2075,14 @@ func (resource *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKin resource.ResponsiveGridLayoutKind = &dashboardResponsiveGridLayoutKind return nil + case "RowsLayout": + var dashboardRowsLayoutKind DashboardRowsLayoutKind + if err := json.Unmarshal(raw, &dashboardRowsLayoutKind); err != nil { + return err + } + + resource.RowsLayoutKind = &dashboardRowsLayoutKind + return nil case "TabsLayout": var dashboardTabsLayoutKind DashboardTabsLayoutKind if err := json.Unmarshal(raw, &dashboardTabsLayoutKind); err != nil { @@ -2158,19 +2170,20 @@ func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingD } // +k8s:openapi-gen=true -type DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind struct { +type DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind struct { GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"` RowsLayoutKind *DashboardRowsLayoutKind `json:"RowsLayoutKind,omitempty"` ResponsiveGridLayoutKind *DashboardResponsiveGridLayoutKind `json:"ResponsiveGridLayoutKind,omitempty"` + TabsLayoutKind *DashboardTabsLayoutKind `json:"TabsLayoutKind,omitempty"` } -// NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind creates a new DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind object. -func NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind() *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind { - return &DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind{} +// NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind creates a new DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind object. +func NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind() *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind { + return &DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind{} } -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind` as JSON. -func (resource DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind) MarshalJSON() ([]byte, error) { +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` as JSON. +func (resource DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) MarshalJSON() ([]byte, error) { if resource.GridLayoutKind != nil { return json.Marshal(resource.GridLayoutKind) } @@ -2180,11 +2193,14 @@ func (resource DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind if resource.ResponsiveGridLayoutKind != nil { return json.Marshal(resource.ResponsiveGridLayoutKind) } + if resource.TabsLayoutKind != nil { + return json.Marshal(resource.TabsLayoutKind) + } return []byte("null"), nil } -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind` from JSON. -func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind) UnmarshalJSON(raw []byte) error { +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` from JSON. +func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) UnmarshalJSON(raw []byte) error { if raw == nil { return nil } @@ -2225,6 +2241,14 @@ func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKin resource.RowsLayoutKind = &dashboardRowsLayoutKind return nil + case "TabsLayout": + var dashboardTabsLayoutKind DashboardTabsLayoutKind + if err := json.Unmarshal(raw, &dashboardTabsLayoutKind); err != nil { + return err + } + + resource.TabsLayoutKind = &dashboardTabsLayoutKind + return nil } return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator) @@ -2472,88 +2496,3 @@ func (resource *DashboardStringOrFloat64) UnmarshalJSON(raw []byte) error { return errors.Join(errList...) } - -// +k8s:openapi-gen=true -type DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind struct { - GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"` - RowsLayoutKind *DashboardRowsLayoutKind `json:"RowsLayoutKind,omitempty"` - ResponsiveGridLayoutKind *DashboardResponsiveGridLayoutKind `json:"ResponsiveGridLayoutKind,omitempty"` - TabsLayoutKind *DashboardTabsLayoutKind `json:"TabsLayoutKind,omitempty"` -} - -// NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind creates a new DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind object. -func NewDashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind() *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind { - return &DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind{} -} - -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` as JSON. -func (resource DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) MarshalJSON() ([]byte, error) { - if resource.GridLayoutKind != nil { - return json.Marshal(resource.GridLayoutKind) - } - if resource.RowsLayoutKind != nil { - return json.Marshal(resource.RowsLayoutKind) - } - if resource.ResponsiveGridLayoutKind != nil { - return json.Marshal(resource.ResponsiveGridLayoutKind) - } - if resource.TabsLayoutKind != nil { - return json.Marshal(resource.TabsLayoutKind) - } - return []byte("null"), nil -} - -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind` from JSON. -func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind) UnmarshalJSON(raw []byte) error { - if raw == nil { - return nil - } - - // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. - parsedAsMap := make(map[string]interface{}) - if err := json.Unmarshal(raw, &parsedAsMap); err != nil { - return err - } - - discriminator, found := parsedAsMap["kind"] - if !found { - return errors.New("discriminator field 'kind' not found in payload") - } - - switch discriminator { - case "GridLayout": - var dashboardGridLayoutKind DashboardGridLayoutKind - if err := json.Unmarshal(raw, &dashboardGridLayoutKind); err != nil { - return err - } - - resource.GridLayoutKind = &dashboardGridLayoutKind - return nil - case "ResponsiveGridLayout": - var dashboardResponsiveGridLayoutKind DashboardResponsiveGridLayoutKind - if err := json.Unmarshal(raw, &dashboardResponsiveGridLayoutKind); err != nil { - return err - } - - resource.ResponsiveGridLayoutKind = &dashboardResponsiveGridLayoutKind - return nil - case "RowsLayout": - var dashboardRowsLayoutKind DashboardRowsLayoutKind - if err := json.Unmarshal(raw, &dashboardRowsLayoutKind); err != nil { - return err - } - - resource.RowsLayoutKind = &dashboardRowsLayoutKind - return nil - case "TabsLayout": - var dashboardTabsLayoutKind DashboardTabsLayoutKind - if err := json.Unmarshal(raw, &dashboardTabsLayoutKind); err != nil { - return err - } - - resource.TabsLayoutKind = &dashboardTabsLayoutKind - return nil - } - - return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator) -} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 818002bf235..580015fc2ed 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -54,8 +54,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKindOrGridLayoutRowKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowSpec(ref), @@ -1935,7 +1934,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref common.Refer } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1956,40 +1955,16 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrResponsiveGridL Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind"), }, }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardResponsiveGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind"}, - } -} - -func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "GridLayoutKind": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind"), - }, - }, "RowsLayoutKind": { SchemaProps: spec.SchemaProps{ Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind"), }, }, - "ResponsiveGridLayoutKind": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardResponsiveGridLayoutKind"), - }, - }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardResponsiveGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardResponsiveGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind"}, } } @@ -3597,7 +3572,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref common.Re }, "layout": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind"), }, }, }, @@ -3605,7 +3580,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions"}, } } @@ -3987,7 +3962,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref common.Re }, "layout": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind"), }, }, }, @@ -3995,7 +3970,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind"}, } } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index 31084da6e9f..7b4e371ea2b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -37,12 +37,10 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind,ConditionalRenderingVariableKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutItemKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutRowKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,GridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,ResponsiveGridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,TabsLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind,GridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind,ResponsiveGridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,GridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,ResponsiveGridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,TabsLayoutKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,GridLayoutKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,ResponsiveGridLayoutKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind,RowsLayoutKind diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 0f8992e6d2d..51d61772704 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -12,7 +12,7 @@ func Migrate(dash map[string]interface{}, targetVersion int) error { // If the schema version is older than the minimum version, with migration support, // we don't migrate the dashboard. if inputVersion < schemaversion.MIN_VERSION { - return schemaversion.NewMigrationError("schema version is too old", inputVersion, schemaversion.MIN_VERSION) + return schemaversion.NewMinimumVersionError(inputVersion) } for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ { diff --git a/apps/dashboard/pkg/migration/migrate_test.go b/apps/dashboard/pkg/migration/migrate_test.go index ef2a3468ce0..8130ca80e17 100644 --- a/apps/dashboard/pkg/migration/migrate_test.go +++ b/apps/dashboard/pkg/migration/migrate_test.go @@ -27,7 +27,7 @@ func TestMigrate(t *testing.T) { "schemaVersion": schemaversion.MIN_VERSION - 1, }, schemaversion.MIN_VERSION) - var minVersionErr = schemaversion.NewMigrationError("schema version is too old", schemaversion.MIN_VERSION-1, schemaversion.MIN_VERSION) + var minVersionErr = schemaversion.NewMinimumVersionError(schemaversion.MIN_VERSION - 1) require.ErrorAs(t, err, &minVersionErr) }) diff --git a/apps/dashboard/pkg/migration/schemaversion/errors.go b/apps/dashboard/pkg/migration/schemaversion/errors.go index ee98593219d..ec01e229b8e 100644 --- a/apps/dashboard/pkg/migration/schemaversion/errors.go +++ b/apps/dashboard/pkg/migration/schemaversion/errors.go @@ -35,5 +35,5 @@ type MinimumVersionError struct { } func (e *MinimumVersionError) Error() string { - return fmt.Errorf("input schema version is below minimum version. input: %d minimum: %d", e.inputVersion, MIN_VERSION).Error() + return fmt.Errorf("dashboard schema version %d cannot be migrated to latest version %d - migration path only exists for versions greater than %d", e.inputVersion, LATEST_VERSION, MIN_VERSION).Error() } diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index aada9c2c3b4..54b28bd61c4 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -37,7 +37,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -68,15 +68,15 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 129a8163693..75beaf4a44e 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -66,8 +66,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -167,8 +167,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -189,12 +189,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index b8162370ed4..501d08545b1 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -38,7 +38,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -69,15 +69,15 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 129a8163693..75beaf4a44e 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -66,8 +66,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -167,8 +167,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -189,12 +189,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/conf/defaults.ini b/conf/defaults.ini index ba04540e320..2b7861b0ebe 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -26,6 +26,13 @@ plugins = data/plugins # folder that contains provisioning config files that grafana will apply on startup and while running. provisioning = conf/provisioning +# Directories that are permitted to contain local repositories. +# This is a list. Each entry is delimited by a pipe (|). No leading or trailing spaces are supported. +# These do not need to be absolute paths, in which case they'll be relative to the path where you are running Grafana. +# Empty entries will return an error, unless the string is just a single pipe. +# Example: permitted_provisioning_paths = /tmp|/etc/grafana/repositories|conf/provisioning +permitted_provisioning_paths = devenv/dev-dashboards|conf/provisioning + #################################### Server ############################## [server] # Protocol (http, https, h2, socket) @@ -932,6 +939,8 @@ key_file = key_id = role_attribute_path = role_attribute_strict = false +org_attribute_path = +org_mapping = groups_attribute_path = auto_sign_up = false url_login = false @@ -1515,6 +1524,16 @@ max_age = # Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. max_annotations_to_keep = +[unified_alerting.prometheus_conversion] +# Configuration options for converting Prometheus alerting and recording rules to Grafana rules. +# These settings affect rules created via the Prometheus conversion API. + +# Offset the rule evaluation time for imported rules by a specified duration in the past. +# This offset is applied and saved to the rule query during the conversion process from Prometheus to Grafana format. +# The setting only affects rules imported after the configuration change is made and does not modify existing rules. +# Accepts duration formats like: 30s, 1m, 1h. +rule_query_offset = 1m + [recording_rules] # Enable recording rules. You must provide write credentials below. enabled = false @@ -1532,8 +1551,8 @@ basic_auth_password = timeout = 10s # Default data source UID to write to if not specified in the rule definition. -# Only has effect if the grafanaManagedRecordRulesDatasources feature toggle is enabled. -default_datasource_uid = +# Only has effect if the grafanaManagedRecordingRulesDatasources feature toggle is enabled. +default_datasource_uid = # Optional custom headers to include in recording rule write requests. [recording_rules.custom_headers] @@ -2089,8 +2108,6 @@ delete_access_policy_timeout = 5s domain = grafana.net # Folder used to store snapshot files. Defaults to the home dir snapshot_folder = "" -# Link to form to give feedback on the feature -feedback_url = https://docs.google.com/forms/d/e/1FAIpQLSeEE33vhbSpR8A8S1A1ocZ1ByVRRwiRl1GZr2FSrEer_tSa8w/viewform?usp=sf_link # How frequently should the frontend UI poll for changes while resources are migrating frontend_poll_interval = 2s # Controls how the Alert Rules are migrated. Available choices: "paused" and "unchanged". Default: "paused". diff --git a/conf/provisioning/sample/classic-dashboard.json b/conf/provisioning/sample/classic-dashboard.json new file mode 100644 index 00000000000..d0746c0b20b --- /dev/null +++ b/conf/provisioning/sample/classic-dashboard.json @@ -0,0 +1,297 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1348, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod\n\n---\ntext = $Text", + "mode": "markdown" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Markdown (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "code": { + "language": "json", + "showLineNumbers": true, + "showMiniMap": false + }, + "content": "{\n \"datacenter\": $datacenter,\n \"server\": $server,\n \"pod\": $pod\n \"text\": $Text\n}\n", + "mode": "code" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "JSON (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "

Data center

\n

$datacenter

\n\n

server

\n

$server

\n\n

pod

\n

$pod

\n\n

Text

\n

$Text

", + "mode": "html" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "HTML (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 7, + "options": { + "code": { + "language": "markdown", + "showLineNumbers": true, + "showMiniMap": true + }, + "content": "## Data center\n$datacenter\n\n### server\n$server\n\n#### pod = \n$pod\n", + "mode": "code" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Markdown (code w/ with variables)", + "type": "text" + } + ], + "refresh": false, + "schemaVersion": 37, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "datacenter", + "options": [], + "query": { + "query": "*", + "refId": "gdev-testdata-datacenter-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "$datacenter.*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "server", + "options": [], + "query": { + "query": "$datacenter.*", + "refId": "gdev-testdata-server-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": ["AAA", "ACB"], + "value": ["AAA", "ACB"] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "$datacenter.$server.*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [], + "query": { + "query": "$datacenter.$server.*", + "refId": "gdev-testdata-pod-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "temp", + "value": "temp" + }, + "hide": 0, + "name": "Text", + "options": [ + { + "selected": true, + "text": "temp", + "value": "temp" + } + ], + "query": "temp", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + }, + "timezone": "", + "title": "Text options", + "uid": "WZ7AhQiVz", + "version": 1, + "weekStart": "" +} diff --git a/conf/provisioning/sample/dashboard-a.json b/conf/provisioning/sample/dashboard-a.json new file mode 100644 index 00000000000..482d093c23d --- /dev/null +++ b/conf/provisioning/sample/dashboard-a.json @@ -0,0 +1,36 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "ignored?", + "namespace": "default", + "uid": "ignored", + "resourceVersion": "also ignored", + "creationTimestamp": "2024-11-15T19:54:07Z", + "annotations": { + "hello": "world:" + }, + "labels": { + "region": "west" + } + }, + "spec": { + "title": "Green dashboard from provisioning", + "panels": [ + { + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 0 + }, + "options": { + "content": "

This dashboard was loaded from provisioning 🎉🎉🎉🎉🎉🎉

", + "mode": "html" + }, + "transparent": true, + "type": "text" + } + ] + } +} diff --git a/conf/provisioning/sample/sample-playlist.json b/conf/provisioning/sample/sample-playlist.json new file mode 100644 index 00000000000..26473e8bfc8 --- /dev/null +++ b/conf/provisioning/sample/sample-playlist.json @@ -0,0 +1,25 @@ +{ + "kind": "Playlist", + "apiVersion": "playlist.grafana.app/v0alpha1", + "metadata": { + "name": "adcmdrw", + "namespace": "default", + "uid": "8a2b984d-4663-4182-861b-edec2f987dff", + "resourceVersion": "1731336353828005", + "creationTimestamp": "2024-11-11T14:45:53Z" + }, + "spec": { + "title": "Playlist from provisioning", + "interval": "5m", + "items": [ + { + "type": "dashboard_by_uid", + "value": "xCmMwXdVz" + }, + { + "type": "dashboard_by_tag", + "value": "panel-tests" + } + ] + } +} diff --git a/conf/sample.ini b/conf/sample.ini index 15f94510ba6..2c44d2be8f4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -26,6 +26,13 @@ # folder that contains provisioning config files that grafana will apply on startup and while running. ;provisioning = conf/provisioning +# Directories that are permitted to contain local repositories. +# This is a list. Each entry is delimited by a pipe (|). No leading or trailing spaces are supported. +# These do not need to be absolute paths, in which case they'll be relative to the path where you are running Grafana. +# Empty entries will return an error, unless the string is just a single pipe. +# Example: permitted_provisioning_paths = /tmp|/etc/grafana/repositories|conf/provisioning +;permitted_provisioning_paths = devenv/dev-dashboards|conf/provisioning + #################################### Server #################################### [server] # Protocol (http, https, h2, socket) @@ -132,7 +139,7 @@ # Set to true or false to enable or disable high availability mode. # When it's set to false some functions will be simplified and only run in-process # instead of relying on the database. -# +# # Only set it to false if you run only a single instance of Grafana. ;high_availability = true @@ -901,6 +908,8 @@ ;key_id = some-key-id ;role_attribute_path = ;role_attribute_strict = false +;org_attribute_path = +;org_mapping = ;groups_attribute_path = ;auto_sign_up = false ;url_login = false @@ -1496,6 +1505,16 @@ max_age = # Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. max_annotations_to_keep = +[unified_alerting.prometheus_conversion] +# Configuration options for converting Prometheus alerting and recording rules to Grafana rules. +# These settings affect rules created via the Prometheus conversion API. + +# Offset the rule evaluation time for imported rules by a specified duration in the past. +# This offset is applied and saved to the rule query during the conversion process from Prometheus to Grafana format. +# The setting only affects rules imported after the configuration change is made and does not modify existing rules. +# Accepts duration formats like: 30s, 1m, 1h. +rule_query_offset = 1m + #################################### Recording Rules ##################### [recording_rules] # Enable recording rules. You must provide write credentials below. @@ -1514,8 +1533,8 @@ basic_auth_password = timeout = 30s # Default data source UID to write to if not specified in the rule definition. -# Only has effect if the grafanaManagedRecordRulesDatasources feature toggle is enabled. -default_datasource_uid = +# Only has effect if the grafanaManagedRecordingRulesDatasources feature toggle is enabled. +default_datasource_uid = # Optional custom headers to include in recording rule write requests. [recording_rules.custom_headers] @@ -1987,8 +2006,6 @@ default_datasource_uid = ;domain = grafana-dev.net # Folder used to store snapshot files. Defaults to the home dir ;snapshot_folder = "" -# Link to form to give feedback on the feature -;feedback_url = "" # How frequently should the frontend UI poll for changes while resources are migrating ;frontend_poll_interval = 2s # Controls how the Alert Rules are migrated. Available choices: "paused" and "unchanged". Default: "paused". diff --git a/contribute/backend/recommended-practices.md b/contribute/backend/recommended-practices.md index e1d940614a8..4a96a0f3f72 100644 --- a/contribute/backend/recommended-practices.md +++ b/contribute/backend/recommended-practices.md @@ -119,7 +119,7 @@ For example: Today it's only possible to provision data sources and dashboards, but we want to support it throughout Grafana in the future. -### 109 - Use `context.Context` +## 109 - Use `context.Context` **State:** Completed. diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index da3669615ab..6084df4ce66 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -19,8 +19,13 @@ We recommend using [Homebrew](https://brew.sh/) for installing any missing depen brew install git brew install go brew install node@22 -brew install corepack +``` + +In the repository enable and install yarn via corepack + +``` corepack enable +corepack install ``` ### Windows diff --git a/docs/sources/administration/data-source-management/teamlbac/_index.md b/docs/sources/administration/data-source-management/teamlbac/_index.md index 4bf449af022..af7c8f180cb 100644 --- a/docs/sources/administration/data-source-management/teamlbac/_index.md +++ b/docs/sources/administration/data-source-management/teamlbac/_index.md @@ -50,7 +50,7 @@ This feature addresses two common challenges faced by Grafana users: LBAC for data sources lets Grafana Teams use the same dashboard with different access control rules. To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for Loki Data Source](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). -To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for Prometheus Data Source](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/). +To set up LBAC for data sources for a Prometheus data source, refer to [Configure LBAC for Prometheus Data Source](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/). ## Logs and Metrics Filtering with LBAC diff --git a/docs/sources/administration/roles-and-permissions/access-control/_index.md b/docs/sources/administration/roles-and-permissions/access-control/_index.md index 888006f4ac7..a6815849f49 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/_index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/_index.md @@ -169,6 +169,8 @@ To interact with the API and view or modify basic roles permissions, refer to [t You cannot use a service account to modify basic roles via the RBAC API. To update basic roles, you must be a Grafana administrator and use basic authentication with the request. {{% /admonition %}} +For Cloud customers, contact Support to reset roles. + ### Fixed roles Grafana Enterprise includes the ability for you to assign discrete fixed roles to users, teams, and service accounts. This gives you fine-grained control over user permissions than you would have with basic roles alone. These roles are called "fixed" because you cannot change or delete fixed roles. You can also create _custom_ roles of your own; see more information in the [custom roles section](#custom-roles) below. diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index baab890cbef..beb5fef013c 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -100,8 +100,6 @@ The following list contains role-based access control actions. | `folders:delete` |
  • `folders:*`
  • `folders:uid:*`
| Delete one or more folders and their subfolders. | | `folders:read` |
  • `folders:*`
  • `folders:uid:*`
| Read one or more folders and their subfolders. | | `folders:write` |
  • `folders:*`
  • `folders:uid:*`
| Update one or more folders and their subfolders. | -| `groupsync.mappings:read` | None | List group attribute sync mappings. To use this permission, enable the `groupAttributeSync` feature toggle. | -| `groupsync.mappings:write` | None | List, create, update, and delete group attribute sync mappings. To use this permission, enable the `groupAttributeSync` feature toggle. | | `ldap.config:reload` | None | Reload the LDAP configuration. | | `ldap.status:read` | None | Verify the availability of the LDAP server or servers. | | `ldap.user:read` | None | Read users via LDAP. | @@ -205,6 +203,20 @@ The following list contains role-based access control actions used by Grafana Ad | `grafana-adaptive-metrics-app.exemptions:read` | None | Read recommendation exemptions. | | `grafana-adaptive-metrics-app.exemptions:write` | None | Create, update, and delete recommendation exemptions. | +### Cloud Access Policies action definitions + +The following list contains role-based access control actions used by Cloud Access Policies. + +| Action | Applicable scopes | Description | +| ------------------------ | ----------------- | ------------------------------------------------------------------- | +| `grafana-auth-app:write` | None | Create, read, update, and delete access policies for Grafana Cloud. | + +{{< admonition type="warning" >}} +Granting the `grafana-auth-app:write` permission is equivalent to assigning the Admin role to a user in Grafana, as it allows them to manage all stack service accounts. This provides significant privileges and should be assigned with caution. +{{< /admonition >}} + +For more information on Cloud Access Policies and how to use them, see [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/). + ### Grafana Alerting Notification action definitions To use these permissions, enable the `alertingApiServer` feature toggle. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 8458e4d9355..193c872d58b 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -50,17 +50,17 @@ refs: Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud). {{% /admonition %}} -The following tables list permissions associated with basic and fixed roles. +The following tables list permissions associated with basic and fixed roles. This does not include basic role assignments added by plugins or apps. ## Basic role assignments -| Basic role | UID | Associated fixed roles | Description | -| ------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Grafana Admin | `basic_grafana_admin` | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer`
`fixed:library.panels:creator`
`fixed:library.panels:reader`
`fixed:library.panels:general.reader`
`fixed:library.panels:writer`
`fixed:library.panels:general.writer`
`fixed:groupsync:writer`
`fixed:migrationassistant:migrator` | Default [Grafana server administrator](/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) assignments. | -| Admin | `basic_admin` | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer`
`fixed:library.panels:creator`
`fixed:library.panels:reader`
`fixed:library.panels:general.reader`
`fixed:library.panels:writer`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer`
`fixed:groupsync:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | -| Editor | `basic_editor` | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:creator`
`fixed:library.panels:general.reader`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | -| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:general.reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | -| No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) | +| Basic role | UID | Associated fixed roles | Description | +| ------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Grafana Admin | `basic_grafana_admin` | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer`
`fixed:library.panels:creator`
`fixed:library.panels:reader`
`fixed:library.panels:general.reader`
`fixed:library.panels:writer`
`fixed:library.panels:general.writer`
`fixed:migrationassistant:migrator` | Default [Grafana server administrator](/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) assignments. | +| Admin | `basic_admin` | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer`
`fixed:library.panels:creator`
`fixed:library.panels:reader`
`fixed:library.panels:general.reader`
`fixed:library.panels:writer`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | +| Editor | `basic_editor` | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:creator`
`fixed:library.panels:general.reader`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | +| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:general.reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | +| No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) | ## Fixed role definitions @@ -115,8 +115,6 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage | `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. | | `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | | `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:groupsync:reader` | `fixed_tLIbDrE6kw93sKqooF8GVS9BF4E` | `groupsync.mappings:read` | List all group attribute sync mappings. To use this role, enable the `groupAttributeSync` feature toggle. | -| `fixed:groupsync:writer` | `fixed_q7XUYx_efzxxsVmWhQgpiYClwBs` | `groupsync.mappings:read`
`groupsync.mappings:write` | Create, read, update, and delete all group attribute sync mappings. To use this role, enable the `groupAttributeSync` feature toggle. | | `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | | `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`
`folders:read` | Create library panel at the root level. | | `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. | @@ -165,16 +163,7 @@ There is only one exclusion at this moment. Role `fixed:alerting.provisioning:wr For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder](ref:plan-rbac-rollout-strategy-create-a-custom-role-to-access-alerts-in-a-folder). -### Grafana OnCall roles (beta) - -{{% admonition type="note" %}} -Available from Grafana 9.4 in early access. -{{% /admonition %}} - -{{% admonition type="note" %}} -This feature is behind the `accessControlOnCall` feature toggle. -You can enable feature toggles through configuration file or environment variables. See configuration [docs](/docs/grafana//setup-grafana/configure-grafana/#feature_toggles) for details. -{{% /admonition %}} +### Grafana OnCall roles If you are using [Grafana OnCall](ref:oncall), you can try out the integration between Grafana OnCall and RBAC. For a detailed list of the available OnCall RBAC roles, refer to the table in [Available Grafana OnCall RBAC roles and granted actions](ref:available-grafana-oncall-rbac-roles--granted-actions). diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md index 020bf9c982c..46304ed2000 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md @@ -24,6 +24,9 @@ refs: destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#grafana-adaptive-metrics-action-definitions - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/#grafana-adaptive-metrics-action-definitions + cloud-access-policies-action-definitions: + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions rbac-role-definitions: - pattern: /docs/grafana/ destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/ @@ -62,7 +65,7 @@ The following list contains app plugins that have fine-grained RBAC support. | App plugin | App plugin ID | App plugin permission documentation | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | n/a | +| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) | | [Adaptive metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) | | [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a | | [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) | diff --git a/docs/sources/administration/service-accounts/_index.md b/docs/sources/administration/service-accounts/_index.md index b853496c094..dc2799d9e3e 100644 --- a/docs/sources/administration/service-accounts/_index.md +++ b/docs/sources/administration/service-accounts/_index.md @@ -63,6 +63,8 @@ refs: You can use a service account to run automated workloads in Grafana, such as dashboard provisioning, configuration, or report generation. Create service accounts and tokens to authenticate applications, such as Terraform, with the Grafana API. +A service account is not the same as a Grafana [user](/docs/grafana/latest/administration/user-management/). The role of a service account is primarily to automate work using the Grafana API. + {{< admonition type="note" >}} Service accounts replace [API keys](ref:migrate-api-keys) as the primary way to authenticate applications that interact with Grafana. {{< /admonition >}} diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 215ed1926dc..3da03cef5b7 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -96,6 +96,11 @@ refs: destination: /docs/grafana//alerting/configure-notifications/template-notifications/manage-notification-templates/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/manage-notification-templates/ + jira: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-jira/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-jira/ configure-grafana-alerts: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/ @@ -142,6 +147,7 @@ Each contact point integration has its own configuration options and setup proce - [Google Chat](ref:gchat) - [Grafana OnCall](ref:oncall) - Kafka REST Proxy +- [Jira](ref:jira) - Line - [Microsoft Teams](ref:teams) - [MQTT](ref:mqtt) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md new file mode 100644 index 00000000000..892529c87f5 --- /dev/null +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md @@ -0,0 +1,74 @@ +--- +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/configure-jira/ +description: Configure the Jira integration to receive notifications when your alerts are firing +keywords: + - grafana + - alerting + - Jira + - integration +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Jira +title: Configure Jira for Alerting +weight: 121 +refs: + configure-contact-points: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/ +--- + +# Configure Jira for Alerting + +Use the Jira integration in a contact point to create issues in your Jira instance when alerts fire. The integration supports both Jira Cloud and Jira Server/Data Center installations. + +## Before you begin + +Before you begin, ensure you have the following: + +- A Jira instance (Cloud, Server, or Data Center) +- API access credentials for Jira +- Appropriate permissions to create issues in your target Jira project + +## Configure Jira for a contact point + +To create a contact point with a Jira integration, complete the following steps: + +1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. +2. Click **+ Add contact point**. +3. Enter a name for the contact point. +4. From the **Integration** list, select **Jira**. +5. Set up the required [settings](#required-settings) for your Jira configuration. +6. Click **Save contact point**. + +For more details on contact points, including how to test them and enable notifications, refer to [Configure contact points](/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/). + +### Required Settings + +| Key | Description | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| URL | The URL of the REST API of your Jira instance. Supported versions: `2` and `3` (e.g., `https://your-domain.atlassian.net/rest/api/3`). | +| Basic Auth User | Username for authentication. For Jira Cloud, use your email address. | +| Basic Auth Password | Password or personal token. For Jira Cloud, you need to obtain a personal token [here](https://id.atlassian.com/manage-profile/security/api-tokens) and use it as the password. | +| API Token | An alternative to basic authentication, a bearer token is used to authorize the API requests. See [Jira documentation](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) for more information. | +| Project Key | The project key identifying the project where issues will be created. Project keys are unique identifiers for a project. | +| Issue Type | The type of issue to create (e.g., `Task`, `Bug`, `Incident`). Make sure that you specify a type that is available in your project. | + +### Optional Settings + +| Key | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | The issue title. Supports templating. Max length is 255 characters. | +| Description | The description of the issue. Depending on version of the API, it can be a text, Markdown, or JSON (v3 API only). Maximum size of the field is 32kb.
**Note:** JSON is not limited by the client, and if it exceeds the size, the API is likely to reject the request. | +| Labels | Custom labels can be added to organize and filter issues created in Jira. Supports templating, allowing dynamic label generation based on alert data. | +| Priority | The priority level of the issue (e.g., `Low`, `Medium`, `High`, `Critical`).
Ensure that the priority value matches the available options for your Jira instance. You can customize priority levels in Jira [here](https://support.atlassian.com/jira-cloud-administration/docs/configure-priorities/). | +| Resolve Transition | The transition name to move the issue to a resolved state when an alert is resolved. Ensure that the value matches a valid transition available in your Jira workflow for the specified issue type. If this field is empty, the issue will not be transitioned to Done. | +| Reopen Transition | The transition name to move the issue back to an open state when an alert reoccurs. Ensure that the value matches a valid transition available in your Jira workflow for the specified issue type. If this field is empty, the issue will not be reopened. | +| Reopen Duration | The time duration (in minutes) to control whether to reopen an issue that was closed within this duration or create a new one. If not specified, the most recent issue that matches the deduplication key will be updated and reopened (if reopen transition is specified). | +| Ignored Resolution | Specify a resolution status that should be ignored when searching for existing issues. For example, issues with this resolution will not be reopened or updated by subsequent alerts. | +| Deduplication Key Field | Custom field to store the deduplication key. Must be a text field.
If not specified, the deduplication key is added to labels in the format of `ALERT(hash sum)`. See [Jira documentation](https://support.atlassian.com/jira-cloud-administration/docs/create-a-custom-field/) for how to create custom fields. | +| Fields | Allows to configure custom fields of Jira issue. The field name should be of the format like `customfield_10001`. | diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md index 5438c8c377b..79b137c1a7d 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md @@ -74,9 +74,14 @@ When an alert rule evaluation results in a `No Data` or `Error` state, Grafana A - `alertname`: Either `DatasourceNoData` or `DatasourceError` depending on the state. - `datasource_uid`: The UID of the data source that caused the state. +- `rulename`: The name of the alert rule that originated the alert. + +Note that `DatasourceNoData` and `DatasourceError` alert instances are independent from the original alert instance. They have different labels, which means existing silences, mute timings, and notification policies applied to the original alert may not apply to them. You can manage these alerts like regular ones by using their labels to apply actions such as adding a silence, routing via notification policies, and more. +If the alert rule is configured to send notifications directly to a selected contact point (instead of using notification policies), the `DatasourceNoData` and `DatasourceError` alerts are also sent to that contact point. Any additional notification settings defined in the alert rule, such as muting or grouping, are preserved. + ### Lifecycle of stale alert instances An alert instance is considered stale if its dimension or series has disappeared from the query results entirely for two evaluation intervals. @@ -85,9 +90,9 @@ Stale alert instances that are in the **Alerting**, **No Data**, or **Error** st ## Modify the `No Data` or `Error` state -In [Configure no data and error handling](ref:no-data-and-error-handling), you can change the default behaviour when the evaluation returns no data or an error. You can set the alert instance state to `Alerting`, `Normal`, or keep the last state. +These states are supported only for Grafana-managed alert rules. -Note that `No Data` and `Error` states are supported only for Grafana-managed alert rules. +In [Configure no data and error handling](ref:no-data-and-error-handling), you can change the default behaviour when the evaluation returns no data or an error. You can set the alert instance state to `Alerting`, `Normal`, `Error`, or `Keep Last State`. {{< figure src="/media/docs/alerting/alert-rule-configure-no-data-and-error-v2.png" alt="A screenshot of the `Configure no data and error handling` option in Grafana Alerting." max-width="500px" >}} diff --git a/docs/sources/alerting/fundamentals/notifications/contact-points.md b/docs/sources/alerting/fundamentals/notifications/contact-points.md index 702bed9cdce..18264d037d3 100644 --- a/docs/sources/alerting/fundamentals/notifications/contact-points.md +++ b/docs/sources/alerting/fundamentals/notifications/contact-points.md @@ -44,6 +44,7 @@ A contact point includes one or more contact point integrations for sending aler - Email - Google Chat - Grafana Oncall +- Jira - Kafka REST Proxy - Line - Microsoft Teams diff --git a/docs/sources/alerting/monitor-status/view-alert-rules.md b/docs/sources/alerting/monitor-status/view-alert-rules.md index 2d7a404b90b..f521800f5bb 100644 --- a/docs/sources/alerting/monitor-status/view-alert-rules.md +++ b/docs/sources/alerting/monitor-status/view-alert-rules.md @@ -61,6 +61,8 @@ For details on how rule states and alert instance states are displayed, refer to ## View, compare and restore alert rules versions. +You can view, compare, and restore previous alert rule versions. The number of alert rule versions is limited to a maximum of 10 alert rule versions for free users, and a maximum of 100 stored alert rule versions for paid tier users. + To view or restore previous versions for an alert rule, complete the following steps. 1. Navigate to **Alerts & IRM -> Alerting -> Alert rules**. diff --git a/docs/sources/dashboards/share-dashboards-panels/_index.md b/docs/sources/dashboards/share-dashboards-panels/_index.md index d424a4b75fb..41231f250f4 100644 --- a/docs/sources/dashboards/share-dashboards-panels/_index.md +++ b/docs/sources/dashboards/share-dashboards-panels/_index.md @@ -41,7 +41,7 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/share-dashboards-panels/shared-dashboards/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana/grafana-cloud/visualizations/dashboards/share-dashboards-panels/shared-dashboards/ + destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/shared-dashboards/ configure-report: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/create-reports/#create-a-report diff --git a/docs/sources/datasources/google-cloud-monitoring/_index.md b/docs/sources/datasources/google-cloud-monitoring/_index.md index 94dfe06d7a3..61b37b747fd 100644 --- a/docs/sources/datasources/google-cloud-monitoring/_index.md +++ b/docs/sources/datasources/google-cloud-monitoring/_index.md @@ -55,6 +55,42 @@ Only users with the organization administrator role can add data sources. Once you've added the Google Cloud Monitoring data source, you can [configure it](#configure-the-data-source) so that your Grafana instance's users can create queries in its [query editor](query-editor/) and apply [annotations](#annotations) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore). +## Before you begin + +Complete the following before you configure the data source. + +### Configure Google authentication + +Before you can request data from Google Cloud Monitoring, you must configure authentication. +All requests to Google APIs are performed on the server-side by the Grafana backend. + +For authentication options and configuration details, refer to [Google authentication](google-authentication/). + +When configuring Google authentication, keep in mind the following additional steps related to Google Cloud Monitoring. + +#### Configure a GCP Service Account + +When you [create a Google Cloud Platform (GCP) Service Account and key file](google-authentication/#create-a-gcp-service-account-and-key-file), the Service Account must have the **Monitoring Viewer** role (**Role > Select a role > Monitoring > Monitoring Viewer**): + +{{< figure src="/static/img/docs/v71/cloudmonitoring_service_account_choose_role.png" max-width="600px" class="docs-image--no-shadow" caption="Choose role" >}} + +#### Grant the GCE Default Service Account scope + +If Grafana is running on a Google Compute Engine (GCE) virtual machine, when you [Configure a GCE Default Service Account](google-authentication/#configure-a-gce-default-service-account), you must also grant that Service Account access to the "Cloud Monitoring API" scope. + +### Enable necessary Google Cloud Platform APIs + +Before you can request data from Google Cloud Monitoring, you must first enable necessary APIs on the Google end. + +1. Open the Monitoring and Cloud Resource Manager API pages: + + - [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) + - [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com) + +1. On each page, click the `Enable` button. + + {{< figure src="/static/img/docs/v71/cloudmonitoring_enable_api.png" max-width="450px" class="docs-image--no-shadow" caption="Enable GCP APIs" >}} + ## Configure the data source To configure basic settings for the data source, complete the following steps: @@ -73,41 +109,9 @@ To configure basic settings for the data source, complete the following steps: | **Name** | Sets the name you use to refer to the data source in panels and queries. | | **Default** | Sets whether the data source is pre-selected for new panels. | -### Configure Google authentication - -Before you can request data from Google Cloud Monitoring, you must configure authentication. -All requests to Google APIs are performed on the server-side by the Grafana backend. - -For authentication options and configuration details, refer to [Google authentication](google-authentication/). - -When configuring Google authentication, note these additional Google Cloud Monitoring-specific steps: - -#### Configure a GCP Service Account - -When you [create a Google Cloud Platform (GCP) Service Account and key file](google-authentication/#create-a-gcp-service-account-and-key-file), the Service Account must have the **Monitoring Viewer** role (**Role > Select a role > Monitoring > Monitoring Viewer**): - -{{< figure src="/static/img/docs/v71/cloudmonitoring_service_account_choose_role.png" max-width="600px" class="docs-image--no-shadow" caption="Choose role" >}} - -#### Grant the GCE Default Service Account scope - -If Grafana is running on a Google Compute Engine (GCE) virtual machine, then when you [Configure a GCE Default Service Account](google-authentication/#configure-a-gce-default-service-account), you must also grant that Service Account access to the "Cloud Monitoring API" scope. - -### Enable necessary Google Cloud Platform APIs - -Before you can request data from Google Cloud Monitoring, you must first enable necessary APIs on the Google end. - -1. Open the Monitoring and Cloud Resource Manager API pages: - - - [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) - - [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com) - -1. On each page, click the `Enable` button. - - {{< figure src="/static/img/docs/v71/cloudmonitoring_enable_api.png" max-width="450px" class="docs-image--no-shadow" caption="Enable GCP APIs" >}} - ### Provision the data source -You can define and configure the data source in YAML files as part of Grafana's provisioning system. +You can define and configure the data source in YAML files as part of the Grafana provisioning system. For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources). #### Provisioning examples diff --git a/docs/sources/developers/http_api/_index.md b/docs/sources/developers/http_api/_index.md index 8889f1670c1..86d9ab8d185 100644 --- a/docs/sources/developers/http_api/_index.md +++ b/docs/sources/developers/http_api/_index.md @@ -84,7 +84,6 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Grafana Enterprise includes all of the Grafana OSS APIs as well as the following APIs: - [Data source permissions API](datasource_permissions/) -- [Group attribute sync API](group_attribute_sync/) - [License API](licensing/) - [Query and resource caching API](query_and_resource_caching/) - [Reporting API](reporting/) diff --git a/docs/sources/developers/http_api/dashboard_versions.md b/docs/sources/developers/http_api/dashboard_versions.md index 96960701098..ef25424bbc7 100644 --- a/docs/sources/developers/http_api/dashboard_versions.md +++ b/docs/sources/developers/http_api/dashboard_versions.md @@ -216,6 +216,7 @@ JSON response body schema: Status codes: - **200** - OK +- **400** - Bad request (specified version has the same content as the current dashboard) - **401** - Unauthorized - **404** - Not found (dashboard not found or dashboard version not found) - **500** - Internal server error (indicates issue retrieving dashboard tags from database) diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md index 73a9c54ba39..7a65f7fa56d 100644 --- a/docs/sources/developers/http_api/folder.md +++ b/docs/sources/developers/http_api/folder.md @@ -205,7 +205,7 @@ Status Codes: - **400** – Errors (invalid json, missing or invalid fields, etc) - **401** – Unauthorized - **403** – Access Denied -- **409** - Folder already exists +- **412** - Folder already exists ## Update folder diff --git a/docs/sources/developers/http_api/group_attribute_sync.md b/docs/sources/developers/http_api/group_attribute_sync.md deleted file mode 100644 index f6d5e752297..00000000000 --- a/docs/sources/developers/http_api/group_attribute_sync.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -description: Grafana Group Attribute Sync HTTP API -keywords: - - grafana - - http - - documentation - - api - - group - - member - - enterprise -labels: - products: - - enterprise - - oss -title: Group Attribute Sync HTTP API ---- - -# Group attribute sync API - -The Group Attribute Sync API allows you to configure [group attribute sync feature](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). This API is useful when you want to manage user roles based on group membership in an external system. - -> **Note:** Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/) - -{{% admonition type="note" %}} -This feature is currently in [private preview](https://grafana.com/docs/release-life-cycle/#private-preview) and behind the `groupAttributeSync` feature toggle. Please contact support to have this feature enabled. -{{% /admonition %}} - -## List group mappings - -`GET /api/groupsync/groups` - -**Required permissions** - -| Action | Scope | -| ----------------------- | ----- | -| groupsync.mappings:read | n/a | - -**Example Request**: - -```http -GET /api/groupsync/groups HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -[ - { - "groups": [ - { - "groupID": "group 1", - "mappings": { - "1": { - "roles": [ - "fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI", - "my_custom_role", - ] - } - } - }, - { - "groupID": "group 2", - "mappings": { - "1": { - "roles": [ - "another_role", - ] - } - } - } - ], - "total": 2 - } -] -``` - -Status Codes: - -- **200** - Ok -- **400** - Bad request -- **401** - Unauthorized -- **403** - Permission denied -- **500** - Internal server error - -## Create group mappings - -`POST /api/groupsync/groups/:groupID` - -**Required permissions** - -| Action | Scope | -| ------------------------ | ----- | -| groupsync.mappings:write | n/a | - -**Example Request**: - -```http -POST /api/groupsync/groups/my_group_id HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt - -{ - "roles": [ - "fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI", - "my_custom_role_uid" - ] -} -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "message": "Group mappings created." -} -``` - -Status Codes: - -- **201** - Ok -- **400** - Bad request -- **401** - Unauthorized -- **403** - Permission denied -- **500** - Internal server error - -## Update group mappings - -`PUT /api/groupsync/groups/:groupID` - -This endpoint will replace the existing mappings for the group with the new mappings provided in the request. - -**Required permissions** - -| Action | Scope | -| ------------------------ | ----- | -| groupsync.mappings:write | n/a | - -**Example Request**: - -```http -PUT /api/groupsync/groups/my_group_id HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt - -{ - "roles": [ - "fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI", - "my_custom_role_uid" - ] -} -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "message": "Group mappings set." -} -``` - -Status Codes: - -- **201** - Ok -- **400** - Bad request -- **401** - Unauthorized -- **403** - Permission denied -- **500** - Internal server error - -## Remove group mappings - -`DELETE /api/groupsync/groups/:groupID` - -**Required permissions** - -| Action | Scope | -| ------------------------ | ----- | -| groupsync.mappings:write | n/a | - -**Example Request**: - -```http -DELETE /api/groupsync/groups/my_group_id HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt -``` - -Status Codes: - -- **204** - Ok -- **400** - Bad request -- **401** - Unauthorized -- **403** - Permission denied -- **500** - Internal server error diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 91896ef88f0..8319f19c274 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -10,12 +10,12 @@ "id": { "type": "string", "description": "Unique name of the plugin. If the plugin is published on grafana.com, then the plugin `id` has to follow the naming conventions.", - "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource|secretsmanager)$" + "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource)$" }, "type": { "type": "string", "description": "Plugin type.", - "enum": ["app", "datasource", "panel", "renderer", "secretsmanager"] + "enum": ["app", "datasource", "panel", "renderer"] }, "info": { "type": "object", @@ -183,11 +183,11 @@ "properties": { "id": { "type": "string", - "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource|secretsmanager)$" + "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource)$" }, "type": { "type": "string", - "enum": ["app", "datasource", "panel", "secretsmanager"] + "enum": ["app", "datasource", "panel"] }, "name": { "type": "string" @@ -280,7 +280,7 @@ }, "type": { "type": "string", - "enum": ["dashboard", "page", "panel", "datasource", "secretsmanager"] + "enum": ["dashboard", "page", "panel", "datasource"] }, "name": { "type": "string" @@ -647,7 +647,7 @@ "id": { "type": "string", "description": "A unique identifier for your exposed component. This is used to reference the component in other plugins. It must be in the following format: '{PLUGIN_ID}/name-of-component/v1'.", - "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource|secretsmanager)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" + "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" }, "title": { "type": "string", @@ -670,7 +670,7 @@ "id": { "type": "string", "description": "A unique identifier for your extension point. This is used to reference the extension point in other plugins. It must be in the following format: '{PLUGIN_ID}/name-of-my-extension-point/v1'.", - "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource|secretsmanager)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" + "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" }, "title": { "type": "string", diff --git a/docs/sources/explore/trace-integration.md b/docs/sources/explore/trace-integration.md index 532bb3ca8f2..ecfa1331199 100644 --- a/docs/sources/explore/trace-integration.md +++ b/docs/sources/explore/trace-integration.md @@ -85,15 +85,29 @@ You can expand any span in a trace and view the details, including the span and For more information about spans and traces, refer to [Introduction to tracing](https://grafana.com/docs/tempo/latest/introduction/) in the Tempo documentation. -Span details include: +Span details include span attributes, resource attributes, events, and links. -- **Span attributes** - Key/value pairs that provides context for spans. For example, if the span deals with calling another service via HTTP, an attribute could include the HTTP URL (maybe as the span attribute key `http.url`) and the HTTP status code returned (as the span attribute `http.status_code`). +#### Span and resource attributes -- **Resource attributes** - Key/value pairs that describe the context of how the span was collected. +**Span attributes** are key-value pairs that provide metadata about a specific span. They give context to the operation being performed, such as information about the request, response, or any relevant operational details. For example, if the span deals with calling another service via HTTP, an attribute could include the HTTP URL (maybe as the span attribute key `http.url`) and the HTTP status code returned (as the span attribute `http.status_code`). -Refer to [Span and resource attributes](/docs/tempo//operations/best-practices/#span-and-resource-attributes) for more detail. +{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-span-attributes.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view span attributes" >}} -{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-details.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view span details" >}} +**Resource attributes** are key-value pairs that describe the environment or entity that is producing the trace. They capture static information about the origin of traces, like the application name or the service version. + +{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-resource-attributes.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view span resource attributes" >}} + +Span attributes are specific to a particular operation, while resource attributes are associated with the whole trace or the entire service emitting the spans. Refer to [Span and resource attributes](/docs/tempo//operations/best-practices/#span-and-resource-attributes) for more detail. + +#### Events + +Events are log-like records attached to a span that represent an occurrence during its execution. They record notable moments or occurrences within the span's lifecycle, such as errors, warnings, or checkpoints. If an error occurs during an operation, an event can be added to the span to indicate what went wrong and when. Events include a timestamp, name, and key-value pairs attributes that provide additional context or details about the event. + +{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-events.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view span events" >}} + +#### Links + +Links show relationships between spans that are not in a direct parent-child relationship. They represent associations between spans that happen concurrently or across separate trace trees, linking traces that originated from separate sources but are logically connected, such as background job processing initiated from a web request. You might use links when a trace passes through an asynchronous queue or when correlating traces from different services. ### Span filters diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 15db695b9bc..0a3189d6bc9 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -34,14 +34,14 @@ Grafana Enterprise includes integrations with more ways to authenticate your use Supported auth providers: - [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy#team-sync-enterprise-only) -- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread#group-sync-enterprise-only) -- [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth#configure-group-synchronization) -- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github#configure-group-synchronization) -- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab#configure-group-synchronization) -- [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google#configure-group-synchronization) -- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization) -- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta#configure-group-synchronization-enterprise-only) -- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml#configure-group-synchronization) +- [Azure AD OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#team-sync-enterprise-only) +- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github/#configure-team-synchronization) +- [Generic OAuth integration](https://grafana.com/docs/grafana//configure-security/configure-authentication/generic-oauth#configure-team-synchronization) +- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab/#configure-team-synchronization) +- [Google OAuth](https://grafana.com/docs/grafana//configure-security/configure-authentication/google#configure-team-synchronization) +- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap/#ldap-group-synchronization-for-teams) +- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta#configure-team-synchronization-enterprise-only) +- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml#configure-team-sync) ### Enhanced LDAP integration diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index cfc5a5e29e5..c0a4301d6ae 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -1189,29 +1189,11 @@ Use this transformation to address issues when a data source returns time series #### Available options -##### Multi-frame time series - -Use this option to transform the time series data frame from the wide format to the long format. This is particularly helpful when your data source delivers time series information in a format that needs to be reshaped for optimal compatibility with your visualization. - -**Example: Converting from wide to long format** - -| Timestamp | Value1 | Value2 | -| ------------------- | ------ | ------ | -| 2023-01-01 00:00:00 | 10 | 20 | -| 2023-01-01 01:00:00 | 15 | 25 | - -**Transformed to:** - -| Timestamp | Variable | Value | -| ------------------- | -------- | ----- | -| 2023-01-01 00:00:00 | Value1 | 10 | -| 2023-01-01 00:00:00 | Value2 | 20 | -| 2023-01-01 01:00:00 | Value1 | 15 | -| 2023-01-01 01:00:00 | Value2 | 25 | - ##### Wide time series -Select this option to transform the time series data frame from the long format to the wide format. If your data source returns time series data in a long format and your visualization requires a wide format, this transformation simplifies the process. +Select this option to transform the time series data frame from the long format to the wide format. + +A wide time series combines data into a single frame with one shared, ascending time field. Time fields do not repeat and multiple values extend in separate columns. **Example: Converting from long to wide format** @@ -1229,6 +1211,32 @@ Select this option to transform the time series data frame from the long format | 2023-01-01 00:00:00 | 10 | 20 | | 2023-01-01 01:00:00 | 15 | 25 | +##### Multi-frame time series + +Multi-frame time series break data into multiple frames that all contain two fields: a time field and a numeric value field. Time is always ascending. String values are represented as field labels. + +##### Long time series + +A long time series combines data to one frame, with the first field being an ascending time field. The time field might have duplicates. String values are in separate fields, and there might be more than one. + +**Example: Converting to long format** + +| Value1 | Value2 | Timestamp | +| ------ | ------ | ------------------- | +| 10 | 20 | 2023-01-03 00:00:00 | +| 30 | 40 | 2023-01-02 00:00:00 | +| 50 | 60 | 2023-01-01 00:00:00 | +| 70 | 80 | 2023-01-01 00:00:00 | + +**Transformed to:** + +| Timestamp | Value1 | Value2 | +| ------------------- | ------ | ------ | +| 2023-01-01 00:00:00 | 70 | 80 | +| 2023-01-01 01:00:00 | 50 | 60 | +| 2023-01-02 01:00:00 | 30 | 40 | +| 2023-01-03 01:00:00 | 10 | 20 | + ### Reduce Use this transformation to apply a calculation to each field in the data frame and return a single value. This transformation is particularly useful for consolidating multiple time series data into a more compact, summarized format. Time fields are removed when applying this transformation. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 073ef1ca9fa..84bc6c9d3b3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -73,6 +73,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes | | `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | Yes | | `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes | +| `pluginsSriChecks` | Enables SRI checks for plugin assets | | | `userStorageAPI` | Enables the user storage API | Yes | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | @@ -147,10 +148,8 @@ Experimental features might be changed or removed without prior notice. | `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | | `extraThemes` | Enables extra themes | | `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | | `datasourceAPIServers` | Expose some datasources as apiservers. | | `provisioning` | Next generation provisioning... and git | | `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | @@ -164,7 +163,6 @@ Experimental features might be changed or removed without prior notice. | `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | | `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | -| `kubernetesRestore` | Allow restoring objects in k8s | | `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | | `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | | `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | @@ -181,6 +179,7 @@ Experimental features might be changed or removed without prior notice. | `tableSharedCrosshair` | Enables shared crosshair in table panel | | `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | | `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `secretsManagementAppPlatform` | Enable the secrets management API and services under app platform | | `scopeApi` | In-development feature flag for the scope api using the app platform. | | `sqlExpressions` | Enables SQL Expressions, which can execute SQL queries against data source results. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | @@ -202,7 +201,6 @@ Experimental features might be changed or removed without prior notice. | `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | | `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | | `rolePickerDrawer` | Enables the new role picker drawer design | -| `pluginsSriChecks` | Enables SRI checks for plugin assets | | `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | | `timeRangeProvider` | Enables time pickers sync | | `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | 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 fd1b2773749..fb0501919f2 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 @@ -390,14 +390,14 @@ This setting is ignored if multiple auth providers are configured to use auto lo auto_login = true ``` -### Group sync (Enterprise only) +### Team Sync (Enterprise only) -With group sync you can map your Entra ID groups to teams and roles in Grafana. This allows users to automatically be added to -the correct teams and be granted the correct roles in Grafana. +With Team Sync you can map your Entra ID groups to teams in Grafana so that your users will automatically be added to +the correct teams. You can reference Entra ID groups by group object ID, like `8bab1c86-8fba-33e5-2089-1d1c80ec267d`. -To learn more about group synchronization, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). +To learn more, refer to the [Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) documentation. ## Common troubleshooting @@ -408,21 +408,24 @@ configuring Azure AD authentication in Grafana. To ensure that the token size doesn't exceed HTTP header size limits, Entra ID limits the number of object IDs that it includes in the groups claim. -If a user is member of more groups than the -overage limit (200), then -Entra ID does not emit the groups claim in the token and emits a group overage claim instead. +If a user is member of more groups than the coverage limit (200), then Entra ID does not emit the groups claim in the token and emits a group overage claim instead. > More information in [Groups overage claim](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference#groups-overage-claim) If Grafana receives a token with a group overage claim instead of a groups claim, Grafana attempts to retrieve the user's group membership by calling the included endpoint. -To ensure this functionality works correctly, you must enable [`force_use_graph_api`](./#force-fetching-groups-from-microsoft-graph-api) in your configuration. +The Entra ID `App registration` must include the following API permissions for group overage claim calls to succeed: + +| Permissions name | Type | Admin consent required | Status | +| ---------------------- | --------- | ---------------------- | ------- | +| `GroupMember.Read.All` | Delegated | Yes | Granted | +| `User.Read` | Delegated | No | Granted | + +Admin consent is required for the `GroupMember.Read.All` permission. To grant admin consent, navigate to **API permissions** in the **App registration** and select **Grant admin consent for [your-organization]**. {{% admonition type="note" %}} -The 'App registration' must include the `GroupMember.Read.All` API permission for group overage claim calls to succeed. - -Admin consent might be required for this permission. +You can make Grafana always get group information from the Microsoft Graph API by turning on the [`force_use_graph_api`](./#force-fetching-groups-from-microsoft-graph-api) setting in the configuration. {{% /admonition %}} #### Configure the required Graph API permissions @@ -433,6 +436,10 @@ Admin consent might be required for this permission. 1. Select **Delegated permissions**. 1. Under the **GroupMember** section, select **GroupMember.Read.All**. 1. Click **Add permissions**. +1. Select **Microsoft Graph** from the list of APIs. +1. Select **Delegated permissions**.. +1. In the **Select permissions** pane, under the **User** section, select **User.Read**. +1. Click the **Add permissions** button at the bottom of the page. {{% admonition type="note" %}} Admin consent may be required for this permission. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md index ee6f7c6335f..c8ec1ab03b6 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md @@ -28,16 +28,18 @@ The enhanced LDAP integration adds additional functionality on top of the [LDAP > To control user access with role-based permissions, refer to [role-based access control](../../../../administration/roles-and-permissions/access-control/). -## LDAP group synchronization +## LDAP group synchronization for teams -With enhanced LDAP integration, you can set up synchronization between LDAP groups and Grafana teams and roles. This enables users that are members -of certain LDAP groups to automatically be added to teams and gain roles in Grafana. - -The below example shows an LDAP group member mapped to a Grafana team. +With enhanced LDAP integration, you can set up synchronization between LDAP groups and teams. This enables LDAP users that are members +of certain LDAP groups to automatically be added or removed as members to certain teams in Grafana. ![LDAP group synchronization](/static/img/docs/enterprise/team_members_ldap.png) -To learn more about group synchronization, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). +Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot. +This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also allows you to manually add +a user as member of a team, and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. + +[Learn more about team sync.](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync)
diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 99e9abb057a..7a7612a35f6 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -127,7 +127,7 @@ To integrate your OAuth2 provider with Grafana using our Generic OAuth authentic c. Enable the refresh token on the provider if required. 1. [Configure role mapping](#configure-role-mapping). -1. Optional: [Configure group synchronization](#configure-group-synchronization). +1. Optional: [Configure team synchronization](https://grafana.com/docs/grafana/ **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. +{{< 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. +{{< /admonition >}} ### Configure role mapping @@ -310,21 +312,21 @@ org_attribute_path = roles org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor ``` -## Configure group synchronization +## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). {{< /admonition >}} -Grafana supports synchronization of OAuth2 groups with Grafana teams and roles. This allows automatically assigning users to the appropriate teams or automatically granting them the mapped roles. -Teams and roles get synchronized when the user logs in. +By using Team Sync, you can link your OAuth2 groups to teams within Grafana. This will automatically assign users to the appropriate teams. +Teams for each user are synchronized when the user logs in. Generic OAuth groups can be referenced by group ID, such as `8bab1c86-8fba-33e5-2089-1d1c80ec267d` or `myteam`. For information on configuring OAuth2 groups with Grafana using the `groups_attribute_path` configuration option, refer to [configuration options](#configuration-options). -To learn more about group synchronization, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). +To learn more about Team Sync, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). -#### Group attribute synchronization example +### Team synchronization example Configuration: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index ffde83ab6d1..1ec6047e0de 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -49,7 +49,7 @@ Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature As a Grafana Admin, you can configure GitHub OAuth client from within Grafana using the GitHub UI. To do this, navigate to **Administration > Authentication > GitHub** 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** . If the save was successful, Grafana will apply the new configurations. +After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. If you need to reset changes you made in the UI back to the default values, click **Reset**. After you have reset the changes, Grafana will apply the configuration from the Grafana configuration file (if there is any configuration) or the default values. @@ -110,7 +110,7 @@ To configure GitHub authentication with Grafana, follow these steps: Review the list of other GitHub [configuration options](#configuration-options) and complete them, as necessary. 1. [Configure role mapping](#configure-role-mapping). -1. Optional: [Configure group synchronization](#configure-group-synchronization). +1. Optional: [Configure team synchronization](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). 1. Restart Grafana. You should now see a GitHub login button on the login page and be able to log in or sign up with your GitHub accounts. @@ -216,14 +216,14 @@ allowed_domains = mycompany.com mycompany.org role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer' ``` -## Configure group synchronization +## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and Grafana Cloud. {{< /admonition >}} -Grafana supports synchronization of teams from your GitHub organization with Grafana teams and roles. This allows automatically assigning users to the appropriate teams or granting them the mapped roles. -Teams and roles get synchronized when the user logs in. +By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams. +Teams for each user are synchronized when the user logs in. GitHub teams can be referenced in two ways: @@ -232,7 +232,7 @@ GitHub teams can be referenced in two ways: Examples: `https://github.com/orgs/grafana/teams/developers` or `@grafana/developers`. -To learn more about group synchronization, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). +To learn more about Team Sync, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). ## Configuration options diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 230eeb1873f..4d350ceabd6 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -119,7 +119,7 @@ To configure GitLab authentication with Grafana, follow these steps: a. Set `use_refresh_token` to `true` in `[auth.gitlab]` section in Grafana configuration file. 1. [Configure role mapping](#configure-role-mapping). -1. Optional: [Configure group synchronization](#configure-group-synchronization). +1. Optional: [Configure team synchronization](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). 1. Restart Grafana. You should now see a GitLab login button on the login page and be able to log in or sign up with your GitLab accounts. @@ -242,20 +242,20 @@ use_pkce = true use_refresh_token = true ``` -## Configure group synchronization +## Configure team synchronization -{{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -{{< /admonition >}} +{{% admonition type="note" %}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). +{{% /admonition %}} -Grafana supports synchronization of GitLab groups with Grafana teams and roles. This allows automatically assigning users to the appropriate teams or granting them the mapped roles. -Teams and roles get synchronized when the user logs in. +By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. +Teams for each user are synchronized when the user logs in. GitLab groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`. Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters. Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. -To learn more about group synchronization, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync). +To learn more about Team Sync, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). ## Configuration options diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index ce130da6e0d..9931151c7e5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -165,15 +165,11 @@ This setting is ignored if multiple auth providers are configured to use auto lo auto_login = true ``` -### Configure group synchronization +### Configure team synchronization -{{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -{{< /admonition >}} +With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. -Grafana supports syncing users to teams and roles based on their Google groups. - -To set up group sync for Google OAuth: +To set up team sync for Google OAuth: 1. Enable the Google Cloud Identity API on your [organization's dashboard](https://console.cloud.google.com/apis/api/cloudidentity.googleapis.com/). @@ -187,9 +183,10 @@ To set up group sync for Google OAuth: scopes = openid email profile https://www.googleapis.com/auth/cloud-identity.groups.readonly ``` -The external group ID for a Google group is the group's email address, such as `dev@grafana.com`. +1. Configure team sync in your Grafana team's `External group sync` tab. + The external group ID for a Google group is the group's email address, such as `dev@grafana.com`. -To learn more about how to configure group synchronization, refer to [Configure team sync](../../configure-team-sync/) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync) documentation. +To learn more about Team Sync, refer to [Configure Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). #### Configure allowed groups diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md index 29dfa1f17ae..26ac14c8188 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md @@ -202,13 +202,19 @@ Grafana checks for the presence of a role using the [JMESPath](http://jmespath.o To assign the role to a specific organization include the `X-Grafana-Org-Id` header along with your JWT when making API requests to Grafana. To learn more about the header, please refer to the [documentation](../../../../developers/http_api/#x-grafana-org-id-header). -### JMESPath examples +### Configure role mapping -To ease configuration of a proper JMESPath expression, you can test/evaluate expressions with custom payloads at http://jmespath.org/. +Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from the JWT. -### Role mapping +The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option. +To map the server administrator role, use the `allow_assign_grafana_admin` configuration option. -If the `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role by default. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. +If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option](../../../configure-grafana/#auto_assign_org_role). +You can disable this default role assignment by setting `role_attribute_strict = true`. This setting denies user access if no role or an invalid role is returned after evaluating the `role_attribute_path` and the `org_mapping` expressions. + +You can use the `org_attribute_path` and `org_mapping` configuration options to assign the user to organizations and specify their role. For more information, refer to [Org roles mapping example](#org-roles-mapping-example). If both org role mapping (`org_mapping`) and the regular role mapping (`role_attribute_path`) are specified, then the user will get the highest of the two mapped roles. + +To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads. **Basic example:** @@ -224,9 +230,9 @@ Payload: } ``` -Config: +Configuration: -```bash +```ini role_attribute_path = role ``` @@ -251,12 +257,40 @@ Payload: } ``` -Config: +Configuration: -```bash +```ini role_attribute_path = contains(info.roles[*], 'admin') && 'Admin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer' ``` +**Org roles mapping example** + +In the following example, the , the user has been granted the role of a `Viewer` in the `org_foo` organization, and the role of an `Editor` in the `org_bar` and `org_baz` organizations. + +Payload: + +```json +{ + ... + "info": { + ... + "orgs": [ + "engineer", + "admin", + ], + ... + }, + ... +} +``` + +Configuration: + +```ini +org_attribute_path = info.orgs +org_mapping = engineer:org_foo:Viewer admin:org_bar:Editor *:org_baz:Editor +``` + ### Grafana Admin Role If the `role_attribute_path` property returns a `GrafanaAdmin` role, Grafana Admin is not assigned by default, instead the `Admin` role is assigned. To allow `Grafana Admin` role to be assigned set `allow_assign_grafana_admin = true`. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md index 347d5fbc4dc..c27aa80ae63 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md @@ -94,7 +94,7 @@ roles ``` {{% admonition type="warning" %}} -These scopes do not add group claims to the id_token. Without group claims, group synchronization will not work. Group synchronization is covered further down in this document. +These scopes do not add group claims to the `id_token`. Without group claims, teamsync will not work. Teamsync is covered further down in this document. {{% /admonition %}} 3. For role mapping to work with the example configuration above, @@ -106,18 +106,16 @@ editor viewer ``` -## Group synchronization +## Teamsync -{{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -{{< /admonition >}} +{{% admonition type="note" %}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). +{{% /admonition %}} -By using group synchronization, you can link your Keycloak groups to teams and roles within Grafana. This allows automatically assigning users to the appropriate teams or granting them the mapped roles. -This is useful if you want to give your users access to specific resources based on their group membership. -Teams and roles get synchronized when the user logs in. +[Teamsync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership. -To enable group synchronization, you need to add a `groups` mapper to the client configuration in Keycloak. -This will add the `groups` claim to the id_token. You can then use the `groups` claim to map groups to teams and roles in Grafana. +To enable teamsync, you need to add a `groups` mapper to the client configuration in Keycloak. +This will add the `groups` claim to the id_token. You can then use the `groups` claim to map groups to teams in Grafana. 1. In the client configuration, head to `Mappers` and create a mapper with the following settings: @@ -143,8 +141,6 @@ If you use nested groups containing special characters such as quotes or colons, groups_attribute_path = reverse("Global:department") ``` -To learn more about how to configure group synchronization, refer to [Configure team sync](../../configure-team-sync/) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync) documentation. - ## Enable Single Logout To enable Single Logout, you need to add the following option to the configuration of Grafana: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 4cc50a36f2a..a159e01bd13 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -169,7 +169,7 @@ To integrate your Okta OIDC provider with Grafana using our Okta OIDC integratio 1. Optional: [Configure a refresh token](#configure-a-refresh-token). 1. [Configure role mapping](#configure-role-mapping). -1. Optional: [Configure group synchronization](#configure-group-synchronization-enterprise-only). +1. Optional: [Configure team synchronization](#configure-team-synchronization-enterprise-only). 1. Restart Grafana. You should now see a Okta OIDC login button on the login page and be able to log in or sign up with your OIDC provider. @@ -243,18 +243,20 @@ org_attribute_path = groups org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"] ``` -### Configure group synchronization (Enterprise only) +### Configure team synchronization (Enterprise only) -{{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -{{< /admonition >}} +{{% admonition type="note" %}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](../../../../introduction/grafana-cloud). +{{% /admonition %}} -By using group synchronization, you can link your Okta groups to teams and roles within Grafana. This allows automatically assigning users to the appropriate teams or granting them the mapped roles. -Teams and roles get synchronized when the user logs in. +By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams. + +Map your Okta groups to teams in Grafana so that your users will automatically be added to +the correct teams. Okta groups can be referenced by group names, like `Admins` or `Editors`. -To learn more about how to configure group synchronization, refer to [Configure team sync](../../configure-team-sync/) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync) documentation. +To learn more about Team Sync, refer to [Configure Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). ## Configuration options diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md index 833759f5a2c..29bc37cfbc1 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml-ui/index.md @@ -131,8 +131,8 @@ http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress ![image](https://github.com/user-attachments/assets/23910ab8-20ec-4dfd-8ef6-7dbaec51ac90) -You also need to configure the **Groups attribute** field if you want to use group synchronization. Group sync allows you to automatically map users to Grafana teams or role-based access control roles based on their SAML group membership. -To learn more about how to configure group synchronization, refer to [Configure team sync](../../configure-team-sync/) and [Configure group attribute sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync) documentation. +You also need to configure the **Groups attribute** field if you want to use team sync. Team sync automatically maps users to Grafana teams based on their SAML group membership. +Learn more about [team sync](../../configure-team-sync) and [configuring team sync for SAML](../saml#configure-team-sync). 1. If you want to automatically assign users' roles based on their SAML roles, complete the **Role mapping** section. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index a04761c8b13..2c1d895a594 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -445,15 +445,24 @@ This setting is ignored if multiple auth providers are configured to use auto lo auto_login = true ``` -### Configure group synchronization +### Configure team sync -Group synchronization allows you to map user groups from an identity provider to Grafana teams and roles. +To use SAML Team sync, set [`assertion_attribute_groups`](../../../configure-grafana/enterprise-configuration#assertion_attribute_groups) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. -To use SAML group synchronization, set [`assertion_attribute_groups`](/docs/grafana//setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_groups) to the attribute name where you store user groups. -Then Grafana will use attribute values extracted from SAML assertion to add user to Grafana teams and grant them roles. +{{% admonition type="warning" %}} +Grafana requires the SAML groups attribute to be configured with distinct `AttributeValue` elements for each group. Do not include multiple groups within a single `AttributeValue` delimited by a comma or any other character. Failure to do so will prevent correct group parsing. Example: + +```xml + + admins_group + division_1 + +``` + +{{% /admonition %}} {{% admonition type="note" %}} -Team sync allows you sync users from SAML to Grafana teams, but you must create teams in Grafana before you can use this feature. It does not automatically create teams in Grafana. +Teamsync allows you sync users from SAML to Grafana teams. It does not automatically create teams in Grafana. You need to create teams in Grafana before you can use this feature. {{% /admonition %}} Given the following partial SAML assertion: @@ -483,12 +492,12 @@ The configuration would look like this: assertion_attribute_groups = groups ``` -The following `External Group ID`s would be valid for configuring team sync or role sync in Grafana: +The following `External Group ID`s would be valid for input in the desired team's _External group sync_ tab: - `admins_group` - `division_1` -To learn more about how to configure group synchronization, refer to [Configure team sync](/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync/) and [Configure group attribute sync](/docs/grafana//setup-grafana/configure-security/configure-group-attribute-sync) documentation. +[Learn more about Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/) ### Configure role sync diff --git a/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md b/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md deleted file mode 100644 index ff963122050..00000000000 --- a/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: Learn how to use group attribute sync to synchronize between groups in your authentication provider and Grafana RBAC roles. -labels: - products: - - cloud - - enterprise -title: Configure group attribute sync -weight: 1000 ---- - -# Configure group attribute sync - -Group attribute sync allows you to manage user permissions in Grafana based on group membership sourced from the user's identity provider (IdP). -Groups are mapped to [fixed](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#fixed-roles) and [custom](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#custom-roles) role-based access control roles in Grafana. - -> **Note:** Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). - -{{% admonition type="note" %}} -This feature is currently in [private preview](https://grafana.com/docs/release-life-cycle/#private-preview) and behind the `groupAttributeSync` feature toggle. Please contact support to have this feature enabled. -{{% /admonition %}} - -When a user logs in, Grafana checks the user's external group memberships and the configured group to role mappings to assign the corresponding roles to the user. -If the user's group memberships change or a new mapping is created, the user's role assignments are updated the next time the user logs in. -If a group mapping is removed, the role assignment to users for the group mapping is revoked immediately. - -Role mappings are tied to organizations, so you can have different mappings for different organizations. - -## Supported providers - -- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread#group-sync-enterprise-only) -- [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth#configure-group-synchronization) -- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github#configure-group-synchronization) -- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab#configure-group-synchronization) -- [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google#configure-group-synchronization) -- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization) -- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta#configure-group-synchronization-enterprise-only) -- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml#configure-group-synchronization) - -## Create role mappings for a new group - -For information about creating group mappings via the API, refer to [create group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#create-group-mappings). - -### Before you begin - -Ensure you have permission to create and update group mappings. By default, the organization administrator role is required to create and edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). - -### To create mappings between an external group and RBAC roles - -1. Sign in to Grafana and click **Administration** in the left-side menu. -1. Click **Users and access**. -1. Click **External group sync**. -1. Click **New**. -1. Insert the group identifier for the group that you want to map. -1. Use the role picker to select the roles that you want to map to the group and click **Update**. -1. Click **Save**. - -## Update role mappings for a group - -For information about updating group mappings via the API, refer to [update group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#update-group-mappings). - -### Before you begin - -Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). - -### To update role mappings for an external group - -1. Sign in to Grafana and click **Administration** in the left-side menu. -1. Click **Users and access**. -1. Click **External group sync**. -1. Find the group whose mappings you want to update. -1. Click on the role picker corresponding to the group and select the roles that you want to map. -1. Click **Apply**. - -## Remove role mappings for a group - -For information about deleting group mappings via the API, refer to [delete group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#delete-group-mappings). - -### Before you begin - -Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). - -### To remove role mappings for an external group - -1. Sign in to Grafana and click **Administration** in the left-side menu. -1. Click **Users and access**. -1. Click **External group sync**. -1. Find the group whose mappings you want to remove. -1. Click on the trash bin icon corresponding to the group mappings you want to remove. diff --git a/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md b/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md index f5e66394faf..133a4626fc3 100644 --- a/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md @@ -14,17 +14,17 @@ title: Configure security hardening Security hardening enables you to apply additional security, which can help stop certain vulnerabilities from being exploited by a malicious attacker. -{{% admonition type="note" %}} +{{< admonition type="note" >}} These settings are available in the [grafana.ini configuration file](../../configure-grafana/#configuration-file-location). To apply changes to the configuration file, restart the Grafana server. -{{% /admonition %}} +{{< /admonition >}} ## Additional security for cookies If Grafana uses HTTPS, you can further secure the cookie that the system uses to authenticate access to the web UI. By applying additional security to the cookie, you might mitigate certain attacks that result from an attacker obtaining the cookie value. -{{% admonition type="note" %}} +{{< admonition type="note" >}} Grafana must use HTTPS for the following configurations to work properly. -{{% /admonition %}} +{{< /admonition >}} ### Add a secure attribute to cookies @@ -33,6 +33,7 @@ To provide mitigation against some MITM attacks, add the `Secure` attribute to t Example: ```toml +[security] # Set to true if you host Grafana behind HTTPS. The default value is false. cookie_secure = true ``` @@ -44,13 +45,14 @@ To mitigate almost all CSRF-attacks, set the _cookie_samesite_ option to `strict Example: ```toml +[security] # set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled" cookie_samesite = strict ``` -{{% admonition type="note" %}} +{{< admonition type="note" >}} By setting the SameSite attribute to "strict," only the user clicks within a Grafana instance work. The default option, "lax," does not produce this behavior. -{{% /admonition %}} +{{< /admonition >}} ### Add a prefix to cookie names @@ -60,6 +62,7 @@ Add a prefix to the current cookie name with either `__Secure-` or `__Host-` whe Example: ```toml +[auth] # Login cookie name login_cookie_name = __Host-grafana_session ``` @@ -75,6 +78,7 @@ A content security policy (CSP) is an HTTP response header that controls how the Example: ```toml +[security] # Enable adding the Content-Security-Policy header to your requests. # CSP enables you to control the resources the user agent can load and helps prevent XSS attacks. content_security_policy = true @@ -114,10 +118,24 @@ If set to `true`, the Grafana server hides the running version number for unauth Example: ```toml +[anonymous.auth] # mask the Grafana version number for unauthenticated users hide_version = true ``` +### Enable auth for metrics + +By default, metrics from Grafana itself can be accessed without authentication. This can lead to information leakage. + +Example: + +```toml +[metrics] +# If both are set, basic auth will be required for the metrics endpoints +basic_auth_username = +basic_auth_password = +``` + ### Enforce domain verification If set to `true`, the Grafana server redirects requests that have a Host-header value that is mismatched to the actual domain. This might help to mitigate some DNS rebinding attacks. @@ -125,6 +143,7 @@ If set to `true`, the Grafana server redirects requests that have a Host-header Example: ```toml +[sever] # Redirect to correct domain if host header does not match domain # Prevents DNS rebinding attacks enforce_domain = true diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index dfe6d4f2c99..70544186767 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -28,14 +28,14 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea ## Supported providers - [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy/#team-sync-enterprise-only) -- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread#group-sync-enterprise-only) -- [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth#configure-group-synchronization) -- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github#configure-group-synchronization) -- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab#configure-group-synchronization) -- [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google#configure-group-synchronization) -- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization) -- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta#configure-group-synchronization-enterprise-only) -- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml#configure-group-synchronization) +- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/azuread#team-sync-enterprise-only) +- [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/generic-oauth#configure-team-synchronization) +- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/github#configure-team-synchronization) +- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/gitlab#configure-team-synchronization) +- [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/google#configure-team-sync-for-google-oauth) +- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams) +- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/okta#configure-team-synchronization-enterprise-only) +- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-authentication/saml#configure-team-sync) ## Synchronize a Grafana team with an external group diff --git a/docs/sources/upgrade-guide/upgrade-v11.6/index.md b/docs/sources/upgrade-guide/upgrade-v11.6/index.md new file mode 100644 index 00000000000..1a1e05ad4df --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v11.6/index.md @@ -0,0 +1,22 @@ +--- +description: Guide for upgrading to Grafana v11.6 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '11.6' +title: Upgrade to Grafana v11.6 +menuTitle: Upgrade to v11.6 +weight: 600 +--- + +# Upgrade to Grafana v11.6 + +{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index dfe2b307d4d..5927368bb6b 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 11 +- [What's new in 11.6](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-6) - [What's new in 11.5](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-5/) - [What's new in 11.4](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-4/) - [What's new in 11.3](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-3/) diff --git a/docs/sources/whatsnew/whats-new-in-v11-6.md b/docs/sources/whatsnew/whats-new-in-v11-6.md new file mode 100644 index 00000000000..c4d95598192 --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v11-6.md @@ -0,0 +1,50 @@ +--- +description: Feature and improvement highlights for Grafana v11.6 +keywords: + - grafana + - new + - documentation + - '11.6' + - release notes +labels: + products: + - cloud + - enterprise + - oss +title: What's new in Grafana v11.6 +posts: + - title: Dashboards and visualizations + items: + - docs/grafana-cloud/whats-new/2025-02-11-canvas-one-click-data-links-and-actions.md + - docs/grafana-cloud/whats-new/2025-02-11-one-click-data-links-and-actions-in-visualizations.md + - docs/grafana-cloud/whats-new/2025-02-14-actions-added-to-visualizations.md + - docs/grafana-cloud/whats-new/2025-02-26-new-actionscell-for-table-visualization.md + - docs/grafana-cloud/whats-new/2025-02-19-better-time-region-control-with-cron-syntax.md + - docs/grafana-cloud/whats-new/2025-03-06-improved-performance-in-geomap-visualizations.md + - docs/grafana-cloud/whats-new/2025-03-06-variables-supported-for-all-transformations.md + - title: Alerting + items: + - docs/grafana-cloud/whats-new/2025-03-05-alert-rule-version-history.md + - docs/grafana-cloud/whats-new/2025-03-05-alerting-support-for-jira-service-management-contact-point.md + - title: Data sources + items: + - docs/grafana-cloud/whats-new/2025-02-28-lbac-for-datasources-metrics.md + - title: Plugins + items: + - docs/grafana-cloud/whats-new/2025-03-12-plugin-details-links-improvements.md + - title: Security + items: + - docs/grafana-cloud/whats-new/2025-02-10-auto-migration-of-api-keys-to-service-accounts.md +whats_new_grafana_version: 11.6 +weight: -48 +--- + +# What’s new in Grafana v11.6 + +Welcome to Grafana 11.6! This minor release includes a number of dashboarding features that are now generally available including one-click data links and actions, Cron syntax support for annotations, and WebGL-powered geomaps for better performance. We've also fully migrated from API keys to service accounts in Grafana for better security. Read on to learn more about version history for Grafana Managed Alerts, label-based access control (LBAC) for Mimir metrics, and more in Grafana v11.6. + +{{< youtube id=iF7yxO4nUXQ >}} + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v11.6, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v11.6/). + +{{< docs/whats-new >}} diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index 925e7bf056c..d5f05088d35 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/extensions-test-app", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "private": true, "scripts": { "build": "webpack -c ./webpack.config.ts --env production", diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e/test-plugins/grafana-test-datasource/package.json index e4472e7a923..fce57ed8757 100644 --- a/e2e/test-plugins/grafana-test-datasource/package.json +++ b/e2e/test-plugins/grafana-test-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/grafana-e2etest-datasource", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "private": true, "scripts": { "build": "webpack -c ./webpack.config.ts --env production", diff --git a/go.mod b/go.mod index 10be8a8a6da..0a347474767 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/Azure/azure-storage-blob-go v0.15.0 // @grafana/grafana-backend-group github.com/Azure/go-autorest/autorest v0.11.29 // @grafana/grafana-backend-group github.com/Azure/go-autorest/autorest/adal v0.9.24 // @grafana/grafana-backend-group - github.com/BurntSushi/toml v1.4.0 // @grafana/identity-access-team + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // @grafana/identity-access-team github.com/DATA-DOG/go-sqlmock v1.5.2 // @grafana/grafana-search-and-storage github.com/Masterminds/semver v1.5.0 // @grafana/grafana-backend-group github.com/Masterminds/semver/v3 v3.3.0 // @grafana/grafana-developer-enablement-squad @@ -27,14 +27,14 @@ require ( github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group github.com/alicebob/miniredis/v2 v2.33.0 // @grafana/alerting-backend github.com/andybalholm/brotli v1.1.1 // @grafana/partner-datasources - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // @grafana/plugins-platform-backend + github.com/apache/arrow-go/v18 v18.2.0 // @grafana/plugins-platform-backend github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad - github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources + github.com/aws/aws-sdk-go v1.55.6 // @grafana/aws-datasources github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad - github.com/blevesearch/bleve/v2 v2.4.3 // @grafana/grafana-search-and-storage - github.com/blevesearch/bleve_index_api v1.1.12 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve_index_api v1.2.3 // @grafana/grafana-search-and-storage github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group @@ -44,10 +44,12 @@ require ( github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 // @grafana/grafana-datasources-core-services github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // @grafana/grafana-datasources-core-services - github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group + 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 github.com/getkin/kin-openapi v0.129.0 // @grafana/grafana-app-platform-squad + github.com/go-git/go-billy/v5 v5.6.2 // @grafana/grafana-app-platform-squad + github.com/go-git/go-git/v5 v5.13.2 // @grafana/grafana-app-platform-squad github.com/go-jose/go-jose/v3 v3.0.4 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team @@ -60,12 +62,13 @@ require ( github.com/go-stack/stack v1.8.1 // @grafana/grafana-backend-group github.com/gobwas/glob v0.2.3 // @grafana/grafana-backend-group github.com/gogo/protobuf v1.3.2 // @grafana/alerting-backend - github.com/golang-jwt/jwt/v4 v4.5.1 // @grafana/grafana-backend-group + github.com/golang-jwt/jwt/v4 v4.5.2 // @grafana/grafana-backend-group github.com/golang-migrate/migrate/v4 v4.7.0 // @grafana/grafana-backend-group github.com/golang/mock v1.7.0-rc.1 // @grafana/alerting-backend github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v0.0.4 // @grafana/alerting-backend github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group + github.com/google/go-github/v69 v69.2.0 // @grafana/grafana-app-platform-squad github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group @@ -90,7 +93,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.272.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling @@ -121,12 +124,15 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 // @grafana/grafana-backend-group github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend github.com/microsoft/go-mssqldb v1.8.0 // @grafana/partner-datasources + github.com/migueleliasweb/go-github-mock v1.1.0 // @grafana/grafana-app-platform-squad github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c //@grafana/identity-access-team github.com/mocktools/go-smtp-mock/v2 v2.3.1 // @grafana/grafana-backend-group github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-backend - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-backend + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect; @grafana/alerting-backend github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group + github.com/open-feature/go-sdk v1.14.1 // @grafana/grafana-backend-group + github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 // @grafana/grafana-backend-group github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369 // @grafana/identity-access-team github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250121233318-0eae96a39570 // @grafana/identity-access-team github.com/openfga/openfga v1.8.6 // @grafana/identity-access-team @@ -159,7 +165,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel v1.35.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // @grafana/grafana-backend-group @@ -181,7 +187,7 @@ require ( golang.org/x/tools v0.30.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent google.golang.org/api v0.220.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.70.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.71.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.5 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -208,7 +214,8 @@ require ( github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8 // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-app-platform-squad // This needs to be here for other projects that import grafana/grafana @@ -246,10 +253,12 @@ require ( github.com/FZambia/eagle v0.2.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect - github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/Yiling-J/theine-go v0.6.0 // indirect @@ -287,22 +296,14 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.23 // indirect + github.com/blevesearch/go-faiss v1.0.24 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.5 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.0.10 // indirect - github.com/blevesearch/zapx/v11 v11.3.10 // indirect - github.com/blevesearch/zapx/v12 v12.3.10 // indirect - github.com/blevesearch/zapx/v13 v13.3.10 // indirect - github.com/blevesearch/zapx/v14 v14.3.10 // indirect - github.com/blevesearch/zapx/v15 v15.3.16 // indirect - github.com/blevesearch/zapx/v16 v16.1.8 // indirect - github.com/blugelabs/ice v1.0.0 // indirect github.com/blugelabs/ice/v2 v2.0.1 // indirect github.com/bufbuild/protocompile v0.4.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -315,11 +316,12 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect @@ -335,6 +337,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.13.2 // 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 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -342,6 +345,7 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -352,19 +356,20 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.23.2 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-github/v64 v64.0.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect @@ -393,6 +398,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect @@ -406,9 +412,10 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -450,6 +457,7 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // @grafana/grafana-search-and-storage @@ -457,8 +465,9 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // @grafana/grafana-search-and-storage github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -488,6 +497,7 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -505,6 +515,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect @@ -518,7 +529,7 @@ require ( go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // @grafana/grafana-backend-group go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // @grafana/sharing-squad go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -527,19 +538,20 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/kms v0.32.1 // indirect @@ -552,19 +564,19 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -require ( - github.com/open-feature/go-sdk v1.14.1 // @grafana/grafana-backend-group - github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 // @grafana/grafana-backend-group -) +require github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 // @grafana/grafana-search-and-storage require ( - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect + github.com/blevesearch/vellum v1.1.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.1 // indirect + github.com/blevesearch/zapx/v12 v12.4.1 // indirect + github.com/blevesearch/zapx/v13 v13.4.1 // indirect + github.com/blevesearch/zapx/v14 v14.4.1 // indirect + github.com/blevesearch/zapx/v15 v15.4.1 // indirect + github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9 // indirect github.com/bluele/gcache v0.0.2 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect - github.com/onsi/ginkgo/v2 v2.22.0 // indirect - github.com/onsi/gomega v1.36.1 // indirect - github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect + github.com/blugelabs/ice v1.0.0 // indirect ) // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream diff --git a/go.sum b/go.sum index 4255f120999..38ff7f37cb8 100644 --- a/go.sum +++ b/go.sum @@ -643,6 +643,8 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 h1:qFYgLH2zZe3WHpQgUrzeazC+ebDebwAQqS9yE1cP5Bs= +github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2/go.mod h1:09/ALd1AXCTCOfcJYD8+jIYKmFmi6PVCkTsipC18F7E= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -704,8 +706,9 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= @@ -727,7 +730,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -742,6 +744,7 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -765,6 +768,8 @@ github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUr github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= +github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0= @@ -797,11 +802,13 @@ github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -834,8 +841,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.50.29/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -898,14 +905,14 @@ github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZ github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4= -github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY= -github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY= -github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 h1:u6DQJ1k4FKwRNtsrVhIRQenNdtz31way7/LgWCluFzA= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205/go.mod h1:nSmFOQ7M264rKoM3jf63Gl2G+ylCgZGovPgL6ZEQYzU= +github.com/blevesearch/bleve_index_api v1.2.3 h1:aJJPhTb30zc68qg2HbvCZLAF1Eh5Hhm296mDAUFaUeg= +github.com/blevesearch/bleve_index_api v1.2.3/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc= -github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/go-faiss v1.0.24 h1:K79IvKjoKHdi7FdiXEsAhxpMuns0x4fM0BO93bW5jLI= +github.com/blevesearch/go-faiss v1.0.24/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= @@ -914,8 +921,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+ github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= +github.com/blevesearch/scorch_segment_api/v2 v2.3.5 h1:t6NVTdS8xp1xHMJoe5WjxdZWLKhsIIb6kB0U49rhAno= +github.com/blevesearch/scorch_segment_api/v2 v2.3.5/go.mod h1:7gG1vsL3lu2L+7RuEtZ7PJhxGYE+AV9zjxTSHuXFMdM= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -925,20 +932,20 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ= github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= -github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= -github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= -github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= -github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= -github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= -github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= -github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= -github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= -github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= -github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= -github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= -github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= -github.com/blevesearch/zapx/v16 v16.1.8 h1:Bxzpw6YQpFs7UjoCV1+RvDw6fmAT2GZxldwX8b3wVBM= -github.com/blevesearch/zapx/v16 v16.1.8/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8= +github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= +github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= +github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= +github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= +github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= +github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= +github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= +github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= +github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= +github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= +github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= +github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= +github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9 h1:a5GLOQkJqCnpD/lTfNiEVQnJ1e2CVOR8JFO6u1D7bLE= +github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9/go.mod h1:v8B9VjBtELr2qRO0f3RoRG8RAZg3Jml/5SYuBT0hvGw= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM= @@ -1012,8 +1019,8 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -1034,6 +1041,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -1126,8 +1135,8 @@ github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCw github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= -github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -1147,8 +1156,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -1174,6 +1183,8 @@ github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJ github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= @@ -1183,6 +1194,14 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -1328,8 +1347,8 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVr github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1349,10 +1368,10 @@ github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -1416,8 +1435,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -1439,6 +1458,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN 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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= +github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= +github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -1574,8 +1597,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= @@ -1588,8 +1611,10 @@ github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 h1:h github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:KKIsWpbv88Lwwcvdjon73zFL7vNJvuXLtsSoUjJErTw= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 h1:B7kt2We4CVCWRJGgaIyx8lhZqTeDAk6bspOkGYWoB/I= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= +github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a h1:dMllTcE0R1qvV4rWDehQzxNiHaale1yCzXsVkub07D0= +github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a/go.mod h1:K/fP4kODJmABug5b90PhACUZD6Xh/veEz2b1VRKNyuA= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 h1:ivbywO8ZnmzDDkn169qUb9REsCGYAA7H8W6VfqSmCEw= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295/go.mod h1:OugmKouuvgWeOI8Kghram2Pv8b/1SuXRR8/iW068uLk= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1763,6 +1788,8 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -1823,6 +1850,8 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -1835,11 +1864,11 @@ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.2/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1952,6 +1981,8 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= +github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= @@ -2109,10 +2140,12 @@ github.com/phpdave11/gofpdi v1.0.14 h1:jlcDIJ6ObCh3X9nANGEK6RY5wbUKHJ5unBjrzG4i8 github.com/phpdave11/gofpdi v1.0.14/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -2285,8 +2318,11 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -2407,6 +2443,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -2487,8 +2525,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector/pdata v1.22.0 h1:3yhjL46NLdTMoP8rkkcE9B0pzjf2973crn0KKhX5UrI= go.opentelemetry.io/collector/pdata v1.22.0/go.mod h1:nLLf6uDg8Kn5g3WNZwGyu8+kf77SwOqQvMTb5AXEbEY= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= @@ -2499,8 +2537,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= @@ -2929,8 +2967,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3303,15 +3341,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3361,8 +3399,8 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -3415,6 +3453,8 @@ gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMM gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -3427,6 +3467,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/go.work b/go.work index dbda98bd6dd..6d6bd805ff0 100644 --- a/go.work +++ b/go.work @@ -8,10 +8,10 @@ use ( ./.citools/bra ./.citools/cog ./.citools/cue - ./.citools/drone ./.citools/golangci-lint ./.citools/jb ./.citools/lefthook + ./.citools/swagger ./apps/advisor ./apps/alerting/notifications ./apps/dashboard @@ -41,3 +41,7 @@ replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.126.0 replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 + +// Can be removed once Bleve releases v2.5.0. +// We've pinned this for: https://github.com/blevesearch/bleve/commit/af9e3111dadfedf9d30f0448506b4a57fecc8550 +replace github.com/blevesearch/bleve/v2 => github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 diff --git a/go.work.sum b/go.work.sum index c44ae6604ab..04f302eb9ea 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,11 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1 h1:tdpHgTbmbvEIARu+bixzmleMi14+3imnpoFXz+Qzjp4= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= @@ -255,6 +263,7 @@ cloud.google.com/go/gkemulticloud v1.4.0 h1:t2HXXYrICui+rZXScietjU1YdrQDLXpfqqrT cloud.google.com/go/gkemulticloud v1.4.0/go.mod h1:rg8YOQdRKEtMimsiNCzZUP74bOwImhLRv9wQ0FwBUP4= cloud.google.com/go/gkemulticloud v1.5.1 h1:JWe6PDNpNU88ZYvQkTd7w28fgeIs/gg6i0hcjUkgZ3M= cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= +cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= cloud.google.com/go/grafeas v0.3.10 h1:D9uP/DjVHq9ZzCekVd+aNvQEHb3Hkwp8ki9FDnhRRJ0= cloud.google.com/go/grafeas v0.3.10/go.mod h1:Mz/AoXmxNhj74VW0fz5Idc3kMN2VZMi4UT5+UPx5Pq0= cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= @@ -508,6 +517,7 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06 git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= +github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk= @@ -535,7 +545,6 @@ github.com/DataDog/sketches-go v1.4.6 h1:acd5fb+QdUzGrosfNLwrIhqyrbMORpvBy7mE+vH github.com/DataDog/sketches-go v1.4.6/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= -github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= @@ -547,13 +556,22 @@ github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/ github.com/IBM/ibm-cos-sdk-go v1.11.0/go.mod h1:FnWOym0CvrPM0nHoXvceClOEvGVXecPpmVIO5RFjlFk= github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw= github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/KimMachineGun/automemlimit v0.6.1 h1:ILa9j1onAAMadBsyyUJv5cack8Y1WT26yLj/V+ulKp8= github.com/KimMachineGun/automemlimit v0.6.1/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/hcsshim v0.9.6 h1:VwnDOgLeoi2du6dAznfmspNqTiwczvjv4K7NxuY9jsY= github.com/Microsoft/hcsshim v0.9.6/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= @@ -611,6 +629,8 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhi github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc= @@ -633,14 +653,26 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= +github.com/blevesearch/bleve_index_api v1.2.1/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= +github.com/blevesearch/scorch_segment_api/v2 v2.3.3/go.mod h1:LXidEjeenMdbcLKP/UdZi1HJOny61FbhslAh5SgN5Ik= github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= +github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= +github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= +github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= +github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= +github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= +github.com/blevesearch/zapx/v16 v16.1.8/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= @@ -660,15 +692,15 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0= github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= -github.com/centrifugal/centrifuge v0.34.4/go.mod h1:VliWwN01/Iom9tLFJyk4E5FdKcn3cbJhnlt0XTnlolo= -github.com/centrifugal/protocol v0.16.0/go.mod h1:7V5vI30VcoxJe4UD87xi7bOsvI0bmEhvbQuMjrFM2L4= -github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/checkpoint-restore/go-criu/v5 v5.0.0 h1:TW8f/UvntYoVDMN1K2HlT82qH1rb0sOjpGw3m6Ym+i4= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= +github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= @@ -745,6 +777,7 @@ github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= @@ -759,6 +792,7 @@ github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= @@ -790,6 +824,8 @@ github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4 github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc= github.com/dave/rebecca v0.9.1 h1:jxVfdOxRirbXL28vXMvUvJ1in3djwkVKXCq339qhBL0= github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= +github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -802,6 +838,8 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= @@ -814,6 +852,9 @@ github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9X github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= +github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= +github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU= +github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30= @@ -849,7 +890,6 @@ github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -902,6 +942,12 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= @@ -916,8 +962,12 @@ github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= @@ -930,6 +980,8 @@ github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs github.com/google/generative-ai-go v0.18.0 h1:6ybg9vOCLcI/UpBBYXOTVgvKmcUKFRNj+2Cj3GnebSo= github.com/google/generative-ai-go v0.18.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= +github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= @@ -951,6 +1003,8 @@ github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyyc github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -978,6 +1032,7 @@ github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e74 github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d/go.mod h1:HQprw3MmiYj5OUV9CZnkwA1FKDZBmYACuAB3oDvUOmI= github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d/go.mod h1:DjJe5osrW/BKrzN9hAAOSElNWutj1bcriExa7iDP7kA= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= +github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a/go.mod h1:K/fP4kODJmABug5b90PhACUZD6Xh/veEz2b1VRKNyuA= github.com/grafana/grafana/pkg/build v0.0.0-20250220114259-be81314e2118/go.mod h1:STVpVboMYeBAfyn6Zw6XHhTHqUxzMy7pzRiVgk1l0W0= github.com/grafana/grafana/pkg/build v0.0.0-20250227105625-8f465f124924/go.mod h1:Vw0LdoMma64VgIMVpRY3i0D156jddgUGjTQBOcyeF3k= github.com/grafana/grafana/pkg/build v0.0.0-20250227163402-d78c646f93bb/go.mod h1:Vw0LdoMma64VgIMVpRY3i0D156jddgUGjTQBOcyeF3k= @@ -996,9 +1051,12 @@ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hamba/avro/v2 v2.27.0 h1:IAM4lQ0VzUIKBuo4qlAiLKfqALSrFC+zi1iseTtbBKU= github.com/hamba/avro/v2 v2.27.0/go.mod h1:jN209lopfllfrz7IGoZErlDz+AyUJ3vrBePQFZwYf5I= +github.com/hamba/avro/v2 v2.28.0 h1:E8J5D27biyAulWKNiEBhV85QPc9xRMCUCGJewS0KYCE= +github.com/hamba/avro/v2 v2.28.0/go.mod h1:9TVrlt1cG1kkTUtm9u2eO5Qb7rZXlYzoKqPt8TSH+TA= github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= github.com/hashicorp/go-msgpack/v2 v2.1.1 h1:xQEY9yB2wnHitoSzk/B9UjXWRQ67QKu5AOm8aFp8N3I= @@ -1036,6 +1094,8 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 h1:742eGXur0715JMq73 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= github.com/jaegertracing/jaeger v1.57.0 h1:3wDtUUPs6NRYH7+d+y8MilDkLHdpPrVlQ2wbcsA62bs= github.com/jaegertracing/jaeger v1.57.0/go.mod h1:p/1fxIU9hKHl7qEhKC72p2ZYVhvvZvNB73y6V7YyuTs= github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= @@ -1047,10 +1107,13 @@ github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw= github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= +github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= @@ -1082,6 +1145,8 @@ github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= @@ -1104,6 +1169,8 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b h1:11UHH39z1RhZ5dc4y4r/4koJo6IYFgTRMe/LlwRTEw0= github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= @@ -1119,7 +1186,6 @@ github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFK github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M= github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0= @@ -1162,6 +1228,7 @@ github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5 h1:0KqC6/s github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk= github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -1176,6 +1243,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.102.0 h1:R70PpK14trQfL/Vj5oAiGRqX09s2gOWuf6t1Ae5fevQ= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.102.0/go.mod h1:xmy/yFFmB1Epy+czrYMbA+4xeOKvhFqNqYWU6qINeis= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.102.0 h1:N3vWsp3xealy4AX8TovfHG5EKi/k7z+F/8LFP4SVAgo= @@ -1239,6 +1307,7 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c h1:AwcgVYzW1T+QuJ2fc55ceOSCiVaOpdYUNpFj9t7+n9U= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -1264,17 +1333,26 @@ github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7z github.com/prometheus/statsd_exporter v0.26.0 h1:SQl3M6suC6NWQYEzOvIv+EF6dAMYEqIuZy+o4H9F5Ig= github.com/prometheus/statsd_exporter v0.26.0/go.mod h1:GXFLADOmBTVDrHc7b04nX8ooq3azG61pnECNqT7O5DM= github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= +github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71 h1:CNooiryw5aisadVfzneSZPswRWvnVW8hF1bS/vo8ReI= github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/rueidis v1.0.53/go.mod h1:by+34b0cFXndxtYmPAHpoTHO5NkosDlBvhexoTURIxM= github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXUJs= github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/richardartoul/molecule v1.0.0 h1:+LFA9cT7fn8KF39zy4dhOnwcOwRoqKiBkPqKqya+8+U= github.com/richardartoul/molecule v1.0.0/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= @@ -1292,11 +1370,9 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs= github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo= -github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= @@ -1308,8 +1384,9 @@ github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1317,14 +1394,19 @@ github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93wh github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= github.com/substrait-io/substrait v0.57.1 h1:GW8nnYfSowMseHR8Os82/X6lNtQGIK7p4p+lr6r+auw= github.com/substrait-io/substrait v0.57.1/go.mod h1:q9s+tjo+gK0lsA+SqYB0lhojNuxvdPdfYlGUP0hjbrA= +github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6 h1:XqtxwYFCjS4L0o1QD4ipGHCuFG94U0f6BeldbilGQjU= +github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw= github.com/substrait-io/substrait-go v1.2.0 h1:3ZNRkc8FYD7ifCagKEOZQtUcgMceMQfwo2N1NGaK4Q4= github.com/substrait-io/substrait-go v1.2.0/go.mod h1:IPsy24rdjp/buXR+T8ENl6QCnSCS6h+uM8P+GaZez7c= +github.com/substrait-io/substrait-go/v3 v3.9.0 h1:sRJf0ID9q2TPxJ9eH+oAniepMqt9fYW0Hy32CScT2cI= +github.com/substrait-io/substrait-go/v3 v3.9.0/go.mod h1:VG7jCqtUm28bSngHwq86FywtU74knJ25LNX63SZ53+E= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck= github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0= github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E= github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg= github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -1388,6 +1470,8 @@ github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3 github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q= github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= @@ -1408,6 +1492,7 @@ github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw= go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo= go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= @@ -1544,12 +1629,14 @@ go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJC go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.starlark.net v0.0.0-20221020143700-22309ac47eac/go.mod h1:kIVgS18CjmEC3PqMd5kaJSGEifyV/CeB9x506ZJ1Vbk= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1611,7 +1698,9 @@ golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1625,6 +1714,8 @@ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= @@ -1680,7 +1771,9 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go. google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU= +google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250102185135-69823020774d h1:NZBSeFsuFS5YrgHMW/8xfTbzNXMshQPNgq2Yb7xipEs= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250102185135-69823020774d/go.mod h1:s4mHJ3FfG8P6A3O+gZ8TVqB3ufjOl9UG3ANCMMwCHmo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250127172529-29210b9bc287 h1:c/HGC2hBfwgjeBtQMLjfmuS2KG28ngtUpn5XiX8o3rY= @@ -1715,9 +1808,11 @@ google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= @@ -1762,10 +1857,23 @@ modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM= modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus2 v1.5.2 h1:Ui+4tc58mf/W+2arcYCJR903y3zl3ecsI7Fpaaqozyw= +modernc.org/ccorpus2 v1.5.2/go.mod h1:Wifvo4Q/qS/h1aRoC2TffcHsnxwTikmi1AuLANuucJQ= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/lex v1.1.1 h1:prSCNTLw1R4rn7M/RzwsuMtAuOytfyR3cnyM07P+Pas= +modernc.org/lex v1.1.1/go.mod h1:6r8o8DLJkAnOsQaGi8fMoi+Vt6LTbDaCrkUK729D8xM= +modernc.org/lexer v1.0.4 h1:hU7xVbZsqwPphyzChc7nMSGrsuaD2PDNOmzrzkS5AlE= +modernc.org/lexer v1.0.4/go.mod h1:tOajb8S4sdfOYitzCgXDFmbVJ/LE0v1fNJ7annTw36U= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/scannertest v1.0.2 h1:JPtfxcVdbRvzmRf2YUvsDibJsQRw8vKA/3jb31y7cy0= +modernc.org/scannertest v1.0.2/go.mod h1:RzTm5RwglF/6shsKoEivo8N91nQIoWtcWI7ns+zPyGA= +modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= diff --git a/lerna.json b/lerna.json index f39bf8763c4..e0645ae04bc 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", - "version": "11.6.0-pre" + "version": "12.0.0-pre" } diff --git a/package.json b/package.json index 726f78741af..2d04f81fdc0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "repository": "github:grafana/grafana", "scripts": { "build": "NODE_ENV=production nx exec --verbose -- webpack --config scripts/webpack/webpack.prod.js --progress", @@ -277,8 +277,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.3.1", - "@grafana/scenes-react": "6.3.1", + "@grafana/scenes": "6.5.3", + "@grafana/scenes-react": "6.5.3", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index a366ceedb0f..82a8a8c46f9 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -36,9 +36,10 @@ }, "dependencies": { "@braintree/sanitize-url": "7.0.1", - "@grafana/schema": "11.6.0-pre", + "@grafana/schema": "12.0.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", + "@types/systemjs": "6.15.1", "d3-interpolate": "3.0.1", "date-fns": "4.1.0", "dompurify": "3.2.4", diff --git a/packages/grafana-data/src/datetime/parser.test.ts b/packages/grafana-data/src/datetime/parser.test.ts index f19ddb87ae7..0edfd577612 100644 --- a/packages/grafana-data/src/datetime/parser.test.ts +++ b/packages/grafana-data/src/datetime/parser.test.ts @@ -23,6 +23,18 @@ describe('dateTimeParse', () => { expect(date.format()).toEqual('2020-08-20T10:30:20Z'); }); + it('should be able to parse ISO 8601 date strings when useBrowserLocale is true', () => { + systemDateFormats.update({ + fullDate: 'YYYY-MM-DD HH:mm:ss.SSS', + interval: {} as SystemDateFormatsState['interval'], + useBrowserLocale: true, + }); + + const date = dateTimeParse('2025-03-12T07:09:37.253Z', { timeZone: 'browser' }); + expect(date.isValid()).toBe(true); + expect(date.format()).toEqual('2025-03-12T02:09:37-05:00'); + }); + it('should be able to parse array formats used by calendar', () => { const date = dateTimeParse([2020, 5, 10, 10, 30, 20], { timeZone: 'utc' }); expect(date.format()).toEqual('2020-06-10T10:30:20Z'); diff --git a/packages/grafana-data/src/datetime/parser.ts b/packages/grafana-data/src/datetime/parser.ts index 5e1bf608401..6f3473d7858 100644 --- a/packages/grafana-data/src/datetime/parser.ts +++ b/packages/grafana-data/src/datetime/parser.ts @@ -53,12 +53,12 @@ export const dateTimeParse: DateTimeParser = (value, }; const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateTime => { + const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth); if (value.indexOf('now') !== -1) { if (!isValid(value)) { return dateTime(); } - const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth); return parsed || dateTime(); } @@ -70,6 +70,12 @@ const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateT return dateTimeForTimeZone(zone.name, value, format); } + if (format === systemDateFormats.fullDate) { + // We use parsed here to handle case when `use_browser_locale` is true + // We need to pass the parsed value to handle case when value is an ISO 8601 date string + return dateTime(parsed, format); + } + switch (lowerCase(timeZone)) { case 'utc': return toUtc(value, format); diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index c795d315260..cecaf049d4d 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -554,6 +554,7 @@ export { type PluginExtensionFunction, type PluginExtensionEventHelpers, type PluginExtensionPanelContext, + type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context, type PluginExtensionDataSourceConfigContext, type PluginExtensionCommandPaletteContext, type PluginExtensionOpenModalOptions, diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index bc679cd8a0d..a85914216ec 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -199,6 +199,7 @@ export interface GrafanaConfig { trustedTypesDefaultPolicyEnabled: boolean; cspReportOnlyEnabled: boolean; liveEnabled: boolean; + liveMessageSizeLimit: number; /** @deprecated Use `theme2` instead. */ theme: GrafanaTheme; theme2: GrafanaTheme2; @@ -215,7 +216,6 @@ export interface GrafanaConfig { unifiedAlerting: UnifiedAlertingConfig; angularSupportEnabled: boolean; feedbackLinksEnabled: boolean; - secretsManagerPluginEnabled: boolean; supportBundlesEnabled: boolean; secureSocksDSProxyEnabled: boolean; googleAnalyticsId: string | undefined; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index bfe8df1df2f..46f4da08a67 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -271,10 +271,6 @@ export interface FeatureToggles { */ traceQLStreaming?: boolean; /** - * Enables metrics summary queries in the Tempo data source - */ - metricsSummary?: boolean; - /** * Expose some datasources as apiservers. */ datasourceAPIServers?: boolean; @@ -390,10 +386,6 @@ export interface FeatureToggles { */ kubernetesDashboards?: boolean; /** - * Allow restoring objects in k8s - */ - kubernetesRestore?: boolean; - /** * Route the folder and dashboard service requests to k8s */ kubernetesClientDashboardsFolders?: boolean; @@ -561,6 +553,10 @@ export interface FeatureToggles { */ onPremToCloudMigrations?: boolean; /** + * Enable the secrets management API and services under app platform + */ + secretsManagementAppPlatform?: boolean; + /** * Writes the state periodically to the database, asynchronous to rule evaluation */ alertingSaveStatePeriodic?: boolean; @@ -814,6 +810,7 @@ export interface FeatureToggles { unifiedStorageSearchSprinkles?: boolean; /** * Enable permission filtering on unified storage search + * @default true */ unifiedStorageSearchPermissionFiltering?: boolean; /** @@ -822,6 +819,7 @@ export interface FeatureToggles { managedDualWriter?: boolean; /** * Enables SRI checks for plugin assets + * @default false */ pluginsSriChecks?: boolean; /** @@ -1059,4 +1057,8 @@ export interface FeatureToggles { * Enables the unified storage history pruner */ unifiedStorageHistoryPruner?: boolean; + /** + * Enables the unified storage grpc connection pool + */ + unifiedStorageGrpcConnectionPool?: boolean; } diff --git a/packages/grafana-data/src/types/live.ts b/packages/grafana-data/src/types/live.ts index 565a6d3fcae..7f4af737f05 100644 --- a/packages/grafana-data/src/types/live.ts +++ b/packages/grafana-data/src/types/live.ts @@ -12,6 +12,7 @@ export enum LiveChannelScope { Plugin = 'plugin', // namespace = plugin name (singleton works for apps too) Grafana = 'grafana', // namespace = feature Stream = 'stream', // namespace = id for the managed data stream + Watch = 'watch', // namespace = k8s group we will watch } /** diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 8e1be814c96..7770eabe2bc 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -17,7 +17,6 @@ export enum PluginType { datasource = 'datasource', app = 'app', renderer = 'renderer', - secretsmanager = 'secretsmanager', } /** Describes status of {@link https://grafana.com/docs/grafana/latest/plugins/plugin-signatures/ | plugin signature} */ diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 7db55531768..ad5ef90be71 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -184,6 +184,7 @@ export enum PluginExtensionPoints { ExploreToolbarAction = 'grafana/explore/toolbar/action', UserProfileTab = 'grafana/user/profile/tab', TraceViewDetails = 'grafana/traceview/details', + QueryEditorRowAdaptiveTelemetryV1 = 'grafana/query-editor-row/adaptivetelemetry/v1', } export type PluginExtensionPanelContext = { @@ -198,6 +199,12 @@ export type PluginExtensionPanelContext = { data?: PanelData; }; +export type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context = { + /** An ordered list of lower-case [a-z]+ string identifiers to provide context clues of where this component is being embedded and how we might want to consider displaying it */ + contextHints?: string[]; + query?: DataQuery; +}; + export type PluginExtensionDataSourceConfigContext< JsonData extends DataSourceJsonData = DataSourceJsonData, SecureJsonData = {}, diff --git a/packages/grafana-data/src/utils/valueMappings.test.ts b/packages/grafana-data/src/utils/valueMappings.test.ts index 2b60616d850..90686b52895 100644 --- a/packages/grafana-data/src/utils/valueMappings.test.ts +++ b/packages/grafana-data/src/utils/valueMappings.test.ts @@ -185,6 +185,40 @@ describe('Format value with value mappings', () => { }); }); +describe('Range mapping with null From or null To', () => { + expect( + getValueMappingResult( + [ + { + type: MappingType.RangeToText, + options: { + from: 0, + to: null, + result: { text: 'pos' }, + }, + }, + ], + 100 + ) + ).toEqual({ text: 'pos' }); + + expect( + getValueMappingResult( + [ + { + type: MappingType.RangeToText, + options: { + from: null, + to: 0, + result: { text: 'neg' }, + }, + }, + ], + -100 + ) + ).toEqual({ text: 'neg' }); +}); + describe('Format value with regex mappings', () => { it('should return correct regular expression result', () => { const value = 'www.foo.com'; diff --git a/packages/grafana-data/src/utils/valueMappings.ts b/packages/grafana-data/src/utils/valueMappings.ts index d7ab2cf6a24..910cf55511f 100644 --- a/packages/grafana-data/src/utils/valueMappings.ts +++ b/packages/grafana-data/src/utils/valueMappings.ts @@ -35,13 +35,17 @@ export function getValueMappingResult(valueMappings: ValueMapping[], value: any) continue; } - const isNumFrom = !isNaN(vm.options.from!); - if (isNumFrom && valueAsNumber < vm.options.from!) { + const from = vm.options.from ?? -Infinity; + + const isNumFrom = !isNaN(from); + if (isNumFrom && valueAsNumber < from) { continue; } - const isNumTo = !isNaN(vm.options.to!); - if (isNumTo && valueAsNumber > vm.options.to!) { + const to = vm.options.to ?? Infinity; + + const isNumTo = !isNaN(to); + if (isNumTo && valueAsNumber > to) { continue; } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index dc8a0541bcf..7525eb21469 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index fab662fb0ac..449c41eff0f 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 9ce7c7e25c5..7093bbcd4b1 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/flamegraph", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana flamegraph visualization component", "keywords": [ "grafana", @@ -44,8 +44,8 @@ ], "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "11.6.0-pre", - "@grafana/ui": "11.6.0-pre", + "@grafana/data": "12.0.0-pre", + "@grafana/ui": "12.0.0-pre", "@leeoniya/ufuzzy": "1.0.18", "d3": "^7.8.5", "lodash": "4.17.21", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 08bb3641efb..a7079f36870 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -1,6 +1,6 @@ { "name": "@grafana/saga-icons", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "private": true, "description": "Icons for Grafana", "author": "Grafana Labs", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 9bf98a363fc..f6c2a2c4977 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "name": "@grafana/o11y-ds-frontend", "private": true, - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Library to manage traces in Grafana.", "sideEffects": false, "repository": { @@ -18,12 +18,12 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "11.6.0-pre", - "@grafana/e2e-selectors": "11.6.0-pre", + "@grafana/data": "12.0.0-pre", + "@grafana/e2e-selectors": "12.0.0-pre", "@grafana/plugin-ui": "0.10.1", - "@grafana/runtime": "11.6.0-pre", - "@grafana/schema": "11.6.0-pre", - "@grafana/ui": "11.6.0-pre", + "@grafana/runtime": "12.0.0-pre", + "@grafana/schema": "12.0.0-pre", + "@grafana/ui": "12.0.0-pre", "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 384e4c2d42c..d88cf2ea261 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -2,7 +2,7 @@ "name": "@grafana/plugin-configs", "description": "Shared dependencies and files for core plugins", "private": true, - "version": "11.6.0-pre", + "version": "12.0.0-pre", "dependencies": { "tslib": "2.8.1" }, diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index bd1378e8870..43181e20566 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "AGPL-3.0-only", "name": "@grafana/prometheus", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana Prometheus Library", "keywords": [ "typescript" @@ -38,15 +38,16 @@ "dependencies": { "@emotion/css": "11.13.5", "@floating-ui/react": "0.27.5", - "@grafana/data": "11.6.0-pre", - "@grafana/e2e-selectors": "11.6.0-pre", + "@grafana/data": "12.0.0-pre", + "@grafana/e2e-selectors": "12.0.0-pre", "@grafana/plugin-ui": "0.10.1", - "@grafana/runtime": "11.6.0-pre", - "@grafana/schema": "11.6.0-pre", - "@grafana/ui": "11.6.0-pre", + "@grafana/runtime": "12.0.0-pre", + "@grafana/schema": "12.0.0-pre", + "@grafana/ui": "12.0.0-pre", "@hello-pangea/dnd": "17.0.0", "@leeoniya/ufuzzy": "1.0.18", "@lezer/common": "1.2.3", + "@lezer/highlight": "1.2.1", "@lezer/lr": "1.4.2", "@prometheus-io/lezer-promql": "0.301.0", "@reduxjs/toolkit": "2.5.1", diff --git a/packages/grafana-prometheus/src/components/PromQueryField.tsx b/packages/grafana-prometheus/src/components/PromQueryField.tsx index 4c619f88b7a..05cdda9864c 100644 --- a/packages/grafana-prometheus/src/components/PromQueryField.tsx +++ b/packages/grafana-prometheus/src/components/PromQueryField.tsx @@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css'; import { PureComponent, ReactNode } from 'react'; import { + getDefaultTimeRange, isDataFrame, LocalStorageValueProvider, QueryEditorProps, @@ -249,6 +250,7 @@ class PromQueryFieldClass extends PureComponent diff --git a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx index df14c9e02c8..602aeca8f6a 100644 --- a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx +++ b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createTheme } from '@grafana/data'; +import { createTheme, getDefaultTimeRange, TimeRange } from '@grafana/data'; import PromQlLanguageProvider from '../language_provider'; @@ -146,7 +146,7 @@ describe('PrometheusMetricsBrowser', () => { const setupProps = (): BrowserProps => { const mockLanguageProvider = { start: () => Promise.resolve(), - getLabelValues: (name: string) => { + getLabelValues: (timeRange: TimeRange, name: string) => { switch (name) { case 'label1': return ['value1-1', 'value1-2']; @@ -163,7 +163,7 @@ describe('PrometheusMetricsBrowser', () => { // The metrics browser expects both label names and label values. // The labels endpoint with match does not supply label values // and so using it breaks the metrics browser. - fetchSeriesLabels: (selector: string) => { + fetchSeriesLabels: (timeRange: TimeRange, selector: string) => { switch (selector) { case '{label1="value1-1"}': return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] }; @@ -187,6 +187,7 @@ describe('PrometheusMetricsBrowser', () => { lastUsedLabels: [], storeLastUsedLabels: () => {}, deleteLastUsedLabels: () => {}, + timeRange: getDefaultTimeRange(), }; return defaults; diff --git a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx index 1eeb59d35ed..db3ec6c60b4 100644 --- a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx +++ b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx @@ -4,7 +4,7 @@ import { ChangeEvent } from 'react'; import * as React from 'react'; import { FixedSizeList } from 'react-window'; -import { GrafanaTheme2, TimeRange } from '@grafana/data'; +import { getDefaultTimeRange, GrafanaTheme2, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { BrowserLabel as PromLabel, @@ -409,11 +409,15 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component { + return this.props.timeRange ?? getDefaultTimeRange(); + }; + async fetchValues(name: string, selector: string) { const { languageProvider } = this.props; this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`); try { - let rawValues = await languageProvider.getLabelValues(name); + let rawValues = await languageProvider.getLabelValues(this.getTimeRange(), name); // If selector changed, clear loading state and discard result by returning early if (selector !== buildSelector(this.state.labels)) { this.updateLabelState(name, { loading: false }); @@ -444,7 +448,12 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component ({ label: variable, value: variable })); + let timeRange = range; + if (!timeRange) { + timeRange = getDefaultTimeRange(); + } + if (!metric) { // get all the labels - datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => { + datasource.getTagKeys({ timeRange, filters: [] }).then((labelNames: Array<{ text: string }>) => { const names = labelNames.map(({ text }) => ({ label: text, value: text })); setLabels(names, variables); }); @@ -122,13 +127,15 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: const labelToConsider = [{ label: '__name__', op: '=', value: metric }]; const expr = promQueryModeller.renderLabels(labelToConsider); - datasource.languageProvider.fetchLabelsWithMatch(expr).then((labelsIndex: Record) => { - const labelNames = Object.keys(labelsIndex); - const names = labelNames.map((value) => ({ label: value, value: value })); - setLabels(names, variables); - }); + datasource.languageProvider + .fetchLabelsWithMatch(timeRange, expr) + .then((labelsIndex: Record) => { + const labelNames = Object.keys(labelsIndex); + const names = labelNames.map((value) => ({ label: value, value: value })); + setLabels(names, variables); + }); } - }, [datasource, qryType, metric]); + }, [datasource, qryType, metric, range]); const onChangeWithVariableString = ( updateVar: { [key: string]: QueryType | string }, @@ -302,6 +309,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: datasource={datasource} onChange={metricsLabelsChange} variableEditor={true} + timeRange={range ?? getDefaultTimeRange()} /> )} diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx index 97e49df416b..d6c86499c25 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx @@ -105,7 +105,7 @@ const MonacoQueryField = (props: Props) => { // we need only one instance of `overrideServices` during the lifetime of the react component const overrideServicesRef = useRef(getOverrideServices()); const containerRef = useRef(null); - const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource } = props; + const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource, timeRange } = props; const lpRef = useLatest(languageProvider); const historyRef = useLatest(history); @@ -155,7 +155,7 @@ const MonacoQueryField = (props: Props) => { historyProvider: historyRef.current, languageProvider: lpRef.current, }); - const completionProvider = getCompletionProvider(monaco, dataProvider); + const completionProvider = getCompletionProvider(monaco, dataProvider, timeRange); // completion-providers in monaco are not registered directly to editor-instances, // they are registered to languages. this makes it hard for us to have diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts index 4ca3fd894ed..ac2995273e5 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts @@ -1,5 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryFieldProps.ts -import { HistoryItem } from '@grafana/data'; +import { HistoryItem, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from '../../datasource'; import type PromQlLanguageProvider from '../../language_provider'; @@ -17,4 +17,5 @@ export type Props = { onRunQuery: (value: string) => void; onBlur: (value: string) => void; datasource: PrometheusDatasource; + timeRange: TimeRange; }; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts index 7a97ba98740..a778cf74300 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -2,6 +2,7 @@ import { config } from '@grafana/runtime'; import { SUGGESTIONS_LIMIT } from '../../../language_provider'; import { FUNCTIONS } from '../../../promql'; +import { getMockTimeRange } from '../../../test/__mocks__/datasource'; import { filterMetricNames, getCompletions } from './completions'; import { DataProvider, type DataProviderParams } from './data_provider'; @@ -183,6 +184,8 @@ function getSuggestionCountForSituation(situationType: MetricNameSituation, metr } describe.each(metricNameCompletionSituations)('metric name completions in situation %s', (situationType) => { + const timeRange = getMockTimeRange(); + it('should return completions for all metric names when the number of metric names is at or below the limit', async () => { jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.atLimit); const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.atLimit.length); @@ -192,12 +195,12 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // No text input dataProvider.monacoSettings.setInputInRange(''); - let completions = await getCompletions(situation, dataProvider); + let completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(expectedCompletionsCount); // With text input (use fuzzy search) dataProvider.monacoSettings.setInputInRange('name_1'); - completions = await getCompletions(situation, dataProvider); + completions = await getCompletions(situation, dataProvider, timeRange); expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount); }); @@ -210,12 +213,12 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Complex query dataProvider.monacoSettings.setInputInRange('metric name one two three four five'); - let completions = await getCompletions(situation, dataProvider); + let completions = await getCompletions(situation, dataProvider, timeRange); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); // Simple query with fuzzy match dataProvider.monacoSettings.setInputInRange('metric_name_'); - completions = await getCompletions(situation, dataProvider); + completions = await getCompletions(situation, dataProvider, timeRange); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); }); @@ -227,19 +230,19 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Do not cross the metrics names threshold jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.atLimit); dataProvider.monacoSettings.setInputInRange('name_1'); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(false); // Cross the metric names threshold, without text input jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.beyondLimit); dataProvider.monacoSettings.setInputInRange(''); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true); // Cross the metric names threshold, with text input jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.beyondLimit); dataProvider.monacoSettings.setInputInRange('name_1'); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true); }); @@ -253,7 +256,7 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Test with a complex query (> 4 terms) dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more'); - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME'); expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true); @@ -268,7 +271,7 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Test with multiple terms dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5'); - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); @@ -308,6 +311,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should not escape special characters when between quotes', async () => { const situation: Situation = { type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME', @@ -316,7 +321,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('value1'); @@ -333,7 +338,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('"value1"'); @@ -350,6 +355,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should escape special characters when between quotes', async () => { const situation: Situation = { type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME', @@ -358,7 +365,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('value1'); @@ -375,7 +382,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('"value1"'); @@ -392,6 +399,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should handle empty values', async () => { jest.spyOn(dataProvider, 'getLabelValues').mockResolvedValue(['']); @@ -402,7 +411,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('""'); }); @@ -417,7 +426,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('test\\"\\\\value'); }); @@ -432,7 +441,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('"123"'); }); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts index ff1f0490560..af0b51bddd4 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -2,6 +2,7 @@ import UFuzzy from '@leeoniya/ufuzzy'; import { languages } from 'monaco-editor'; +import { TimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; import { prometheusRegularEscape } from '../../../datasource'; @@ -160,14 +161,15 @@ function makeSelector(metricName: string | undefined, labels: Label[]): string { async function getLabelNames( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint return Promise.resolve(dataProvider.getAllLabelNames()); } else { const selector = makeSelector(metric, otherLabels); - return await dataProvider.getSeriesLabels(selector, otherLabels); + return await dataProvider.getSeriesLabels(timeRange, selector, otherLabels); } } @@ -176,9 +178,10 @@ async function getLabelNamesForCompletions( suffix: string, triggerOnInsert: boolean, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - const labelNames = await getLabelNames(metric, otherLabels, dataProvider); + const labelNames = await getLabelNames(metric, otherLabels, dataProvider, timeRange); return labelNames.map((text) => { const isUtf8 = !isValidLegacyName(text); return { @@ -200,31 +203,34 @@ async function getLabelNamesForCompletions( async function getLabelNamesForSelectorCompletions( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider); + return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider, timeRange); } async function getLabelNamesForByCompletions( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider); + return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider, timeRange); } async function getLabelValues( metric: string | undefined, labelName: string, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint - return dataProvider.getLabelValues(labelName); + return dataProvider.getLabelValues(timeRange, labelName); } else { const selector = makeSelector(metric, otherLabels); - return await dataProvider.getSeriesValues(labelName, selector); + return await dataProvider.getSeriesValues(timeRange, labelName, selector); } } @@ -233,9 +239,10 @@ async function getLabelValuesForMetricCompletions( labelName: string, betweenQuotes: boolean, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - const values = await getLabelValues(metric, labelName, otherLabels, dataProvider); + const values = await getLabelValues(metric, labelName, otherLabels, dataProvider, timeRange); return values.map((text) => ({ type: 'LABEL_VALUE', label: text, @@ -248,7 +255,11 @@ function formatLabelValueForCompletion(value: string, betweenQuotes: boolean): s return betweenQuotes ? text : `"${text}"`; } -export function getCompletions(situation: Situation, dataProvider: DataProvider): Promise { +export function getCompletions( + situation: Situation, + dataProvider: DataProvider, + timeRange: TimeRange +): Promise { switch (situation.type) { case 'IN_DURATION': return Promise.resolve(DURATION_COMPLETIONS); @@ -263,16 +274,17 @@ export function getCompletions(situation: Situation, dataProvider: DataProvider) return Promise.resolve([...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]); } case 'IN_LABEL_SELECTOR_NO_LABEL_NAME': - return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider); + return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange); case 'IN_GROUPING': - return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider); + return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange); case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME': return getLabelValuesForMetricCompletions( situation.metricName, situation.labelName, situation.betweenQuotes, situation.otherLabels, - dataProvider + dataProvider, + timeRange ); default: throw new NeverCaseError(situation); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts index 5108401b2fd..def57a02621 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts @@ -1,4 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/index.ts +import { TimeRange } from '@grafana/data'; import type { Monaco, monacoTypes } from '@grafana/ui'; import { CompletionType, getCompletions } from './completions'; @@ -48,7 +49,8 @@ function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): mona export function getCompletionProvider( monaco: Monaco, - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): monacoTypes.languages.CompletionItemProvider { const provideCompletionItems = ( model: monacoTypes.editor.ITextModel, @@ -84,7 +86,8 @@ export function getCompletionProvider( const offset = model.getOffsetAt(positionClone); const situation = getSituation(model.getValue(), offset); - const completionsPromise = situation != null ? getCompletions(situation, dataProvider) : Promise.resolve([]); + const completionsPromise = + situation != null ? getCompletions(situation, dataProvider, timeRange) : Promise.resolve([]); return completionsPromise.then((items) => { // monaco by-default alphabetically orders the items. diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 44e93ebb7d1..082e391ad9b 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -483,7 +483,7 @@ export class PrometheusDatasource return Promise.resolve([]); } - const timeRange = options?.range ?? this.languageProvider.timeRange ?? getDefaultTimeRange(); + const timeRange = options?.range ?? getDefaultTimeRange(); const scopedVars = { ...this.getIntervalVars(), @@ -654,6 +654,10 @@ export class PrometheusDatasource // it is used in metric_find_query.ts // and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx async getTagKeys(options: DataSourceGetTagKeysOptions): Promise { + if (!options.timeRange) { + options.timeRange = getDefaultTimeRange(); + } + if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { const suggestions = await this.languageProvider.fetchSuggestions( options.timeRange, @@ -680,7 +684,10 @@ export class PrometheusDatasource })); const expr = promQueryModeller.renderLabels(labelFilters); - let labelsIndex: Record = await this.languageProvider.fetchLabelsWithMatch(expr); + let labelsIndex: Record = await this.languageProvider.fetchLabelsWithMatch( + options.timeRange, + expr + ); // filter out already used labels return Object.keys(labelsIndex) @@ -689,7 +696,11 @@ export class PrometheusDatasource } // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality - async getTagValues(options: DataSourceGetTagValuesOptions) { + async getTagValues(options: DataSourceGetTagValuesOptions): Promise { + if (!options.timeRange) { + options.timeRange = getDefaultTimeRange(); + } + const requestId = `[${this.uid}][${options.key}]`; if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { return ( @@ -715,7 +726,7 @@ export class PrometheusDatasource if (this.hasLabelsMatchAPISupport()) { return ( - await this.languageProvider.fetchSeriesValuesWithMatch(options.key, expr, requestId, options.timeRange) + await this.languageProvider.fetchSeriesValuesWithMatch(options.timeRange, options.key, expr, requestId) ).map((v) => ({ value: v, text: v, diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index 2b0c7edcaf3..e91845d1f00 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -110,6 +110,8 @@ describe('Language completion provider', () => { }); describe('getSeriesLabels', () => { + const timeRange = getMockTimeRange(); + it('should call labels endpoint', () => { const languageProvider = new LanguageProvider({ ...defaultDatasource, @@ -120,7 +122,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -151,7 +153,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -186,7 +188,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -217,13 +219,15 @@ describe('Language completion provider', () => { }); describe('getSeriesValues', () => { + const timeRange = getMockTimeRange(); + it('should call old series endpoint and should use match[] parameter', () => { const languageProvider = new LanguageProvider({ ...defaultDatasource, } as PrometheusDatasource); const getSeriesValues = languageProvider.getSeriesValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - getSeriesValues('job', '{job="grafana"}'); + getSeriesValues(timeRange, 'job', '{job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -246,7 +250,7 @@ describe('Language completion provider', () => { const requestSpy = jest.spyOn(languageProvider, 'request'); const labelName = 'job'; const labelValue = 'grafana'; - getSeriesValues(labelName, `{${labelName}="${labelValue}"}`); + getSeriesValues(timeRange, labelName, `{${labelName}="${labelValue}"}`); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( `/api/v1/label/${labelName}/values`, @@ -267,7 +271,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const getSeriesValues = languageProvider.getSeriesValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - getSeriesValues('job', '{instance="$instance", job="grafana"}'); + getSeriesValues(timeRange, 'job', '{instance="$instance", job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -288,7 +292,7 @@ describe('Language completion provider', () => { const timeRange = getMockTimeRange(); await languageProvider.start(timeRange); const requestSpy = jest.spyOn(languageProvider, 'request'); - await languageProvider.fetchSeries('{job="grafana"}'); + await languageProvider.fetchSeries(timeRange, '{job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -311,7 +315,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('$metric'); + fetchSeriesLabels(getMockTimeRange(), '$metric'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -332,7 +336,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('metric-with-limit', undefined, 'none'); + fetchSeriesLabels(getMockTimeRange(), 'metric-with-limit', undefined, 'none'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -353,7 +357,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('metric-without-limit', false, 'none'); + fetchSeriesLabels(getMockTimeRange(), 'metric-without-limit', false, 'none'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -619,7 +623,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchLabelValues = languageProvider.fetchLabelValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchLabelValues('$job'); + fetchLabelValues(getMockTimeRange(), '$job'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/interpolated_job/values', @@ -639,7 +643,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchLabelValues = languageProvider.fetchLabelValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchLabelValues('"http.status:sum"'); + fetchLabelValues(getMockTimeRange(), '"http.status:sum"'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/U__http_2e_status:sum/values', @@ -661,7 +665,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesValuesWithMatch('"http.status:sum"', '{__name__="a_utf8_http_requests_total"}'); + fetchSeriesValuesWithMatch(getMockTimeRange(), '"http.status:sum"', '{__name__="a_utf8_http_requests_total"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/U__http_2e_status:sum/values', @@ -681,7 +685,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesValuesWithMatch('"http_status_sum"', '{__name__="a_utf8_http_requests_total"}'); + fetchSeriesValuesWithMatch(getMockTimeRange(), '"http_status_sum"', '{__name__="a_utf8_http_requests_total"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/http_status_sum/values', diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 22c427c979b..04a9f7e6e7b 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -79,7 +79,6 @@ const PREFIX_DELIMITER_REGEX = const secondsInDay = 86400; export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics: string[]; - timeRange: TimeRange; metrics: string[]; metricsMetadata?: PromMetricsMetadata; declare startTask: Promise; @@ -92,7 +91,6 @@ export default class PromQlLanguageProvider extends LanguageProvider { this.datasource = datasource; this.histogramMetrics = []; - this.timeRange = getDefaultTimeRange(); this.metrics = []; Object.assign(this, initialValues); @@ -109,7 +107,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { cleanText(s: string) { const parts = s.split(PREFIX_DELIMITER_REGEX); const last = parts.pop()!; - return last.trimLeft().replace(/"$/, '').replace(/^"/, ''); + return last.trimStart().replace(/"$/, '').replace(/^"/, ''); } get syntax() { @@ -129,16 +127,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { return defaultValue; }; - start = async (timeRange?: TimeRange): Promise => { - this.timeRange = timeRange ?? getDefaultTimeRange(); - + start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { if (this.datasource.lookupsDisabled) { return []; } - this.metrics = (await this.fetchLabelValues('__name__')) || []; + this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || []; this.histogramMetrics = processHistogramMetrics(this.metrics).sort(); - return Promise.all([this.loadMetricsMetadata(), this.fetchLabels()]); + return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]); }; async loadMetricsMetadata() { @@ -186,15 +182,15 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; } - async getSeries(selector: string, withName?: boolean): Promise> { + async getSeries(timeRange: TimeRange, selector: string, withName?: boolean): Promise> { if (this.datasource.lookupsDisabled) { return {}; } try { if (selector === EMPTY_SELECTOR) { - return await this.fetchDefaultSeries(); + return await this.fetchDefaultSeries(timeRange); } else { - return await this.fetchSeriesLabels(selector, withName, REMOVE_SERIES_LIMIT); + return await this.fetchSeriesLabels(timeRange, selector, withName, REMOVE_SERIES_LIMIT); } } catch (error) { // TODO: better error handling @@ -203,11 +199,8 @@ export default class PromQlLanguageProvider extends LanguageProvider { } } - /** - * @param key - */ - fetchLabelValues = async (key: string): Promise => { - const params = this.datasource.getAdjustedInterval(this.timeRange); + fetchLabelValues = async (range: TimeRange, key: string): Promise => { + const params = this.datasource.getAdjustedInterval(range); const interpolatedName = this.datasource.interpolateString(key); const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName)); const url = `/api/v1/label/${interpolatedAndEscapedName}/values`; @@ -215,19 +208,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { return value ?? []; }; - async getLabelValues(key: string): Promise { - return await this.fetchLabelValues(key); + async getLabelValues(range: TimeRange, key: string): Promise { + return await this.fetchLabelValues(range, key); } /** * Fetches all label keys */ - fetchLabels = async (timeRange?: TimeRange, queries?: PromQuery[]): Promise => { - if (timeRange) { - this.timeRange = timeRange; - } + fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[]): Promise => { let url = '/api/v1/labels'; - const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const timeParams = this.datasource.getAdjustedInterval(timeRange); this.labelFetchTs = Date.now().valueOf(); const searchParams = new URLSearchParams({ ...timeParams }); @@ -259,17 +249,15 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Gets series values - * Function to replace old getSeries calls in a way that will provide faster endpoints for new prometheus instances, - * while maintaining backward compatability - * @param labelName - * @param selector + * Function to replace old getSeries calls in a way that will provide faster endpoints + * for new prometheus instances, while maintaining backward compatability */ - getSeriesValues = async (labelName: string, selector: string): Promise => { + getSeriesValues = async (timeRange: TimeRange, labelName: string, selector: string): Promise => { if (!this.datasource.hasLabelsMatchAPISupport()) { - const data = await this.getSeries(selector); + const data = await this.getSeries(timeRange, selector); return data[removeQuotesIfExist(labelName)] ?? []; } - return await this.fetchSeriesValuesWithMatch(labelName, selector); + return await this.fetchSeriesValuesWithMatch(timeRange, labelName, selector); }; /** @@ -280,10 +268,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { * @param requestId */ fetchSeriesValuesWithMatch = async ( + timeRange: TimeRange, name: string, match: string, - requestId?: string, - timeRange: TimeRange = this.timeRange + requestId?: string ): Promise => { const interpolatedName = name ? this.datasource.interpolateString(name) : null; const interpolatedMatch = match ? this.datasource.interpolateString(match) : null; @@ -321,16 +309,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { * @param selector * @param otherLabels */ - getSeriesLabels = async (selector: string, otherLabels: Label[]): Promise => { + getSeriesLabels = async (timeRange: TimeRange, selector: string, otherLabels: Label[]): Promise => { let possibleLabelNames, data: Record; if (!this.datasource.hasLabelsMatchAPISupport()) { - data = await this.getSeries(selector); + data = await this.getSeries(timeRange, selector); possibleLabelNames = Object.keys(data); // all names from prometheus } else { // Exclude __name__ from output otherLabels.push({ name: '__name__', value: '', op: '!=' }); - data = await this.fetchSeriesLabelsMatch(selector); + data = await this.fetchSeriesLabelsMatch(timeRange, selector); possibleLabelNames = Object.keys(data); } @@ -341,31 +329,31 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch labels using the best endpoint that datasource supports. * This is cached by its args but also by the global timeRange currently selected as they can change over requested time. - * @param name - * @param withName */ - fetchLabelsWithMatch = async (name: string, withName?: boolean): Promise> => { + fetchLabelsWithMatch = async ( + timeRange: TimeRange, + name: string, + withName?: boolean + ): Promise> => { if (this.datasource.hasLabelsMatchAPISupport()) { - return this.fetchSeriesLabelsMatch(name, withName); + return this.fetchSeriesLabelsMatch(timeRange, name, withName); } else { - return this.fetchSeriesLabels(name, withName, REMOVE_SERIES_LIMIT); + return this.fetchSeriesLabels(timeRange, name, withName, REMOVE_SERIES_LIMIT); } }; /** * Fetch labels for a series using /series endpoint. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. - * @param name - * @param withName - * @param withLimit */ fetchSeriesLabels = async ( + timeRange: TimeRange, name: string, withName?: boolean, withLimit?: string ): Promise> => { const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(this.timeRange); + const range = this.datasource.getAdjustedInterval(timeRange); let urlParams: UrlParamsType = { ...range, 'match[]': interpolatedName, @@ -385,12 +373,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch labels for a series using /labels endpoint. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. - * @param name - * @param withName */ - fetchSeriesLabelsMatch = async (name: string, withName?: boolean): Promise> => { + fetchSeriesLabelsMatch = async ( + timeRange: TimeRange, + name: string, + withName?: boolean + ): Promise> => { const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(this.timeRange); + const range = this.datasource.getAdjustedInterval(timeRange); const urlParams = { ...range, 'match[]': interpolatedName, @@ -404,11 +394,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels. - * @param match */ - fetchSeries = async (match: string): Promise>> => { + fetchSeries = async (timeRange: TimeRange, match: string): Promise>> => { const url = '/api/v1/series'; - const range = this.datasource.getTimeRangeParams(this.timeRange); + const range = this.datasource.getTimeRangeParams(timeRange); const params = { ...range, 'match[]': match }; return await this.request(url, {}, params, this.getDefaultCacheHeaders()); }; @@ -418,8 +407,8 @@ export default class PromQlLanguageProvider extends LanguageProvider { * because we can cache more aggressively here and also we do not want to invalidate this cache the same way as in * fetchSeriesLabels. */ - fetchDefaultSeries = once(async () => { - const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(key))); + fetchDefaultSeries = once(async (timeRange: TimeRange) => { + const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(timeRange, key))); return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {}); }); @@ -442,12 +431,12 @@ export default class PromQlLanguageProvider extends LanguageProvider { limit?: number, requestId?: string ): Promise => { - if (timeRange) { - this.timeRange = timeRange; + if (!timeRange) { + timeRange = getDefaultTimeRange(); } const url = '/suggestions'; - const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const timeParams = this.datasource.getAdjustedInterval(timeRange); const value = await this.request( url, [], @@ -456,7 +445,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { queries: queries?.map((q) => this.datasource.interpolateString(q.expr, { ...this.datasource.getIntervalVars(), - ...this.datasource.getRangeScopedVars(this.timeRange), + ...this.datasource.getRangeScopedVars(timeRange), }) ), scopes: scopes?.reduce((acc, scope) => { diff --git a/packages/grafana-prometheus/src/metric_find_query.ts b/packages/grafana-prometheus/src/metric_find_query.ts index 69494f0f7fc..12d17e7126f 100644 --- a/packages/grafana-prometheus/src/metric_find_query.ts +++ b/packages/grafana-prometheus/src/metric_find_query.ts @@ -1,7 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/metric_find_query.ts import { chain, map as _map, uniq } from 'lodash'; -import { getDefaultTimeRange, MetricFindValue, TimeRange } from '@grafana/data'; +import { MetricFindValue, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from './datasource'; import { getPrometheusTime } from './language_utils'; @@ -15,19 +15,15 @@ import { import { escapeForUtf8Support, isValidLegacyName } from './utf8_support'; export class PrometheusMetricFindQuery { - range: TimeRange; - constructor( private datasource: PrometheusDatasource, private query: string ) { this.datasource = datasource; this.query = query; - this.range = getDefaultTimeRange(); } process(timeRange: TimeRange): Promise { - this.range = timeRange; const labelNamesRegex = PrometheusLabelNamesRegex; const labelNamesRegexWithMatch = PrometheusLabelNamesRegexWithMatch; const labelValuesRegex = PrometheusLabelValuesRegex; @@ -38,7 +34,7 @@ export class PrometheusMetricFindQuery { if (labelNamesMatchQuery) { const selector = `{__name__=~".*${labelNamesMatchQuery[1]}.*"}`; - return this.datasource.languageProvider.getSeriesLabels(selector, []).then((results) => + return this.datasource.languageProvider.getSeriesLabels(timeRange, selector, []).then((results) => results.map((result) => ({ text: result, })) @@ -54,35 +50,35 @@ export class PrometheusMetricFindQuery { const filter = labelValuesQuery[1]; const label = labelValuesQuery[2]; if (isFilterDefined(filter)) { - return this.labelValuesQuery(label, filter); + return this.labelValuesQuery(label, timeRange, filter); } else { // Exclude the filter part of the expression because it is blank or empty - return this.labelValuesQuery(label); + return this.labelValuesQuery(label, timeRange); } } const metricNamesQuery = this.query.match(metricNamesRegex); if (metricNamesQuery) { - return this.metricNameQuery(metricNamesQuery[1]); + return this.metricNameQuery(metricNamesQuery[1], timeRange); } const queryResultQuery = this.query.match(queryResultRegex); if (queryResultQuery) { - return this.queryResultQuery(queryResultQuery[1]); + return this.queryResultQuery(queryResultQuery[1], timeRange); } // if query contains full metric name, return metric name and label list const expressions = ['label_values()', 'metrics()', 'query_result()']; if (!expressions.includes(this.query)) { - return this.metricNameAndLabelsQuery(this.query); + return this.metricNameAndLabelsQuery(this.query, timeRange); } return Promise.resolve([]); } - labelValuesQuery(label: string, metric?: string) { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + labelValuesQuery(label: string, range: TimeRange, metric?: string) { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { ...(metric && { 'match[]': metric }), start: start.toString(), end: end.toString() }; let escapedLabel = label; @@ -118,9 +114,9 @@ export class PrometheusMetricFindQuery { } } - metricNameQuery(metricFilterPattern: string) { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + metricNameQuery(metricFilterPattern: string, range: TimeRange) { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { start: start.toString(), end: end.toString(), @@ -143,11 +139,11 @@ export class PrometheusMetricFindQuery { }); } - queryResultQuery(query: string) { + queryResultQuery(query: string, range: TimeRange) { const url = '/api/v1/query'; const params = { query, - time: getPrometheusTime(this.range.to, true).toString(), + time: getPrometheusTime(range.to, true).toString(), }; return this.datasource.metadataRequest(url, params).then((result) => { switch (result.data.data.resultType) { @@ -182,9 +178,9 @@ export class PrometheusMetricFindQuery { }); } - metricNameAndLabelsQuery(query: string): Promise { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + metricNameAndLabelsQuery(query: string, range: TimeRange): Promise { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { 'match[]': query, start: start.toString(), diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index ae6353e8b1a..f6f62ffb54b 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -1,7 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx import { useCallback } from 'react'; -import { SelectableValue } from '@grafana/data'; +import { SelectableValue, TimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; import { PrometheusDatasource } from '../../datasource'; @@ -22,6 +22,7 @@ export interface MetricsLabelsSectionProps { onChange: (update: PromVisualQuery) => void; variableEditor?: boolean; onBlur?: () => void; + timeRange: TimeRange; } export function MetricsLabelsSection({ @@ -30,6 +31,7 @@ export function MetricsLabelsSection({ onChange, onBlur, variableEditor, + timeRange, }: MetricsLabelsSectionProps) { // fixing the use of 'as' from refactoring // @ts-ignore @@ -63,7 +65,7 @@ export function MetricsLabelsSection({ const onGetLabelNames = async (forLabel: Partial): Promise => { // If no metric we need to use a different method if (!query.metric) { - await datasource.languageProvider.fetchLabels(); + await datasource.languageProvider.fetchLabels(timeRange); return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k })); } @@ -71,7 +73,7 @@ export function MetricsLabelsSection({ labelsToConsider.push({ label: '__name__', op: '=', value: query.metric }); const expr = promQueryModeller.renderLabels(labelsToConsider); - let labelsIndex: Record = await datasource.languageProvider.fetchLabelsWithMatch(expr); + let labelsIndex: Record = await datasource.languageProvider.fetchLabelsWithMatch(timeRange, expr); // filter out already used labels return Object.keys(labelsIndex) @@ -124,7 +126,7 @@ export function MetricsLabelsSection({ if (!forLabel.label) { return Promise.resolve([]); } - const result = datasource.languageProvider.fetchSeries(promQLExpression); + const result = datasource.languageProvider.fetchSeries(timeRange, promQLExpression); const forLabelInterpolated = datasource.interpolateString(forLabel.label); return result.then((result) => { // This query returns duplicate values, scrub them out @@ -154,7 +156,7 @@ export function MetricsLabelsSection({ const requestId = `[${datasource.uid}][${query.metric}][${forLabel.label}][${forLabel.op}]`; return datasource.languageProvider - .fetchSeriesValuesWithMatch(forLabel.label, promQLExpression, requestId) + .fetchSeriesValuesWithMatch(timeRange, forLabel.label, promQLExpression, requestId) .then((response) => response.map((v) => ({ value: v, label: v }))); }; @@ -169,7 +171,7 @@ export function MetricsLabelsSection({ } // If no metric is selected, we can get the raw list of labels if (!query.metric) { - return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v })); + return (await datasource.languageProvider.getLabelValues(timeRange, forLabel.label)).map((v) => ({ value: v })); } const labelsToConsider = query.labels.filter((x) => x !== forLabel); @@ -191,8 +193,8 @@ export function MetricsLabelsSection({ }; const onGetMetrics = useCallback(() => { - return withTemplateVariableOptions(getMetrics(datasource, query)); - }, [datasource, query, withTemplateVariableOptions]); + return withTemplateVariableOptions(getMetrics(datasource, query, timeRange)); + }, [datasource, query, timeRange, withTemplateVariableOptions]); const MetricSelectComponent = config.featureToggles.prometheusUsesCombobox ? MetricCombobox : MetricSelect; @@ -229,7 +231,8 @@ export function MetricsLabelsSection({ */ async function getMetrics( datasource: PrometheusDatasource, - query: PromVisualQuery + query: PromVisualQuery, + timeRange: TimeRange ): Promise> { // Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we // don't use it with the visual builder and there is no need to run all the start() setup anyway. @@ -245,9 +248,9 @@ async function getMetrics( let metrics: string[]; if (query.labels.length > 0) { const expr = promQueryModeller.renderLabels(query.labels); - metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? []; + metrics = (await datasource.languageProvider.getSeries(timeRange, expr, true))['__name__'] ?? []; } else { - metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; + metrics = (await datasource.languageProvider.getLabelValues(timeRange, '__name__')) ?? []; } return metrics.map((m) => ({ diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx index 8e58f495963..372873eebb1 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx @@ -89,7 +89,7 @@ describe('PromQueryBuilder', () => { it('tries to load metrics without labels', async () => { const { languageProvider, container } = setup(); await openMetricSelect(container); - await waitFor(() => expect(languageProvider.getLabelValues).toHaveBeenCalledWith('__name__')); + await waitFor(() => expect(languageProvider.getLabelValues).toHaveBeenCalledWith(expect.anything(), '__name__')); }); it('tries to load metrics with labels', async () => { @@ -98,7 +98,13 @@ describe('PromQueryBuilder', () => { labels: [{ label: 'label_name', op: '=', value: 'label_value' }], }); await openMetricSelect(container); - await waitFor(() => expect(languageProvider.getSeries).toHaveBeenCalledWith('{label_name="label_value"}', true)); + await waitFor(() => + expect(languageProvider.getSeries).toHaveBeenCalledWith( + expect.anything(), + '{label_name="label_value"}', + expect.anything() + ) + ); }); it('tries to load variables in metric field', async () => { @@ -113,7 +119,10 @@ describe('PromQueryBuilder', () => { const { languageProvider } = setup(); await openLabelNameSelect(); await waitFor(() => - expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith('{__name__="random_metric"}') + expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), + '{__name__="random_metric"}' + ) ); }); @@ -135,6 +144,7 @@ describe('PromQueryBuilder', () => { await openLabelNameSelect(1); await waitFor(() => expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), '{label_name="label_value", __name__="random_metric"}' ) ); @@ -273,7 +283,10 @@ describe('PromQueryBuilder', () => { }); await openLabelNameSelect(); await waitFor(() => - expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith('{__name__="random_metric"}') + expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), + '{__name__="random_metric"}' + ) ); }); @@ -301,6 +314,7 @@ describe('PromQueryBuilder', () => { await openLabelNameSelect(1); await waitFor(() => expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), '{label_name="label_value", __name__="random_metric"}' ) ); diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index 11c79f5932c..a7e0841f634 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { memo, useState } from 'react'; -import { DataSourceApi, PanelData } from '@grafana/data'; +import { DataSourceApi, getDefaultTimeRange, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorRow } from '@grafana/plugin-ui'; @@ -43,7 +43,12 @@ export const PromQueryBuilder = memo((props) => { return ( <> - + {initHints.length ? (
; exploreDefaultTimeOffset = '1h'; diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 2a40243f2ad..964ac05316e 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -35,7 +35,12 @@ export { type UsePluginFunctionsResult, } from './pluginExtensions/usePluginFunctions'; -export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; +export { + isPluginExtensionLink, + isPluginExtensionComponent, + getLimitedComponentsToRender, + renderLimitedComponents, +} from './pluginExtensions/utils'; export { setCurrentUser } from './user'; export { RuntimeDataSource } from './RuntimeDataSource'; export { ScopesContext, type ScopesContextValueState, type ScopesContextValue, useScopes } from './ScopesContext'; diff --git a/packages/grafana-runtime/src/services/live.ts b/packages/grafana-runtime/src/services/live.ts index e3787383995..41c05ac76bd 100644 --- a/packages/grafana-runtime/src/services/live.ts +++ b/packages/grafana-runtime/src/services/live.ts @@ -39,6 +39,20 @@ export interface LiveQueryDataOptions { body: unknown; // processed queries, same as sent to `/api/query/ds` } +/** + * @alpha -- experimental + */ +export interface LivePublishOptions { + /** + * Publish the data over the websocket instead of the HTTP API. + * + * This is not recommended for most use cases. + * + * @experimental + */ + useSocket?: boolean; +} + /** * @alpha -- experimental */ @@ -79,7 +93,7 @@ export interface GrafanaLiveSrv { * * @alpha -- experimental */ - publish(address: LiveChannelAddress, data: unknown): Promise; + publish(address: LiveChannelAddress, data: unknown, options?: LivePublishOptions): Promise; } let singletonInstance: GrafanaLiveSrv; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/utils.test.ts b/packages/grafana-runtime/src/services/pluginExtensions/utils.test.ts deleted file mode 100644 index c4c6e1f10bd..00000000000 --- a/packages/grafana-runtime/src/services/pluginExtensions/utils.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PluginExtension, PluginExtensionTypes } from '@grafana/data'; - -import { isPluginExtensionLink } from './utils'; - -describe('Plugin Extensions / Utils', () => { - describe('isPluginExtensionLink()', () => { - test('should return TRUE if the object is a link extension', () => { - expect( - isPluginExtensionLink({ - id: 'id', - pluginId: 'plugin-id', - type: PluginExtensionTypes.link, - title: 'Title', - description: 'Description', - path: '...', - } as PluginExtension) - ).toBe(true); - - expect( - isPluginExtensionLink({ - id: 'id', - pluginId: 'plugin-id', - type: PluginExtensionTypes.link, - title: 'Title', - description: 'Description', - onClick: () => {}, - } as PluginExtension) - ).toBe(true); - }); - test('should return FALSE if the object is NOT a link extension', () => { - expect( - isPluginExtensionLink({ - type: PluginExtensionTypes.link, - title: 'Title', - description: 'Description', - } as PluginExtension) - ).toBe(false); - - expect( - // @ts-ignore (Right now we only have a single type of extension) - isPluginExtensionLink({ - type: 'unknown', - title: 'Title', - description: 'Description', - path: '...', - } as PluginExtension) - ).toBe(false); - }); - }); -}); diff --git a/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx b/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx new file mode 100644 index 00000000000..e235aace668 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx @@ -0,0 +1,269 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { + ComponentTypeWithExtensionMeta, + PluginExtension, + PluginExtensionComponentMeta, + PluginExtensionTypes, +} from '@grafana/data'; + +import { getLimitedComponentsToRender, isPluginExtensionLink, renderLimitedComponents } from './utils'; + +describe('Plugin Extensions / Utils', () => { + describe('isPluginExtensionLink()', () => { + test('should return TRUE if the object is a link extension', () => { + expect( + isPluginExtensionLink({ + id: 'id', + pluginId: 'plugin-id', + type: PluginExtensionTypes.link, + title: 'Title', + description: 'Description', + path: '...', + } as PluginExtension) + ).toBe(true); + + expect( + isPluginExtensionLink({ + id: 'id', + pluginId: 'plugin-id', + type: PluginExtensionTypes.link, + title: 'Title', + description: 'Description', + onClick: () => {}, + } as PluginExtension) + ).toBe(true); + }); + test('should return FALSE if the object is NOT a link extension', () => { + expect( + isPluginExtensionLink({ + type: PluginExtensionTypes.link, + title: 'Title', + description: 'Description', + } as PluginExtension) + ).toBe(false); + + expect( + // @ts-ignore (Right now we only have a single type of extension) + isPluginExtensionLink({ + type: 'unknown', + title: 'Title', + description: 'Description', + path: '...', + } as PluginExtension) + ).toBe(false); + }); + }); + + describe('getLimitedComponentsToRender()', () => { + test('should return `null` if it receives an empty array of components', () => { + const props = {}; + const components: Array> = []; + const limitedComponents = getLimitedComponentsToRender({ props, components }); + expect(limitedComponents).toEqual(null); + }); + + test('should return all components if no limit is provided', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, undefined, 'id-1'), + createComponent(() =>
Test 2
, undefined, 'id-2'), + createComponent(() =>
Test 3
, undefined, 'id-3'), + ]; + + expect(getLimitedComponentsToRender({ props, components })?.length).toEqual(3); + }); + + test('should limit the number of components', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, undefined, 'id-1'), + createComponent(() =>
Test 2
, undefined, 'id-2'), + createComponent(() =>
Test 3
, undefined, 'id-3'), + createComponent(() =>
Test 4
, undefined, 'id-4'), + createComponent(() =>
Test 5
, undefined, 'id-5'), + ]; + + // Check if the limit is respected + expect(getLimitedComponentsToRender({ props, components, limit: 1 })?.length).toEqual(1); + expect(getLimitedComponentsToRender({ props, components, limit: 3 })?.length).toEqual(3); + + // Check if the right components are selected + const limitedComponents = getLimitedComponentsToRender({ props, components, limit: 3 }); + const rendered = render( + <>{limitedComponents?.map((Component, index) => )} + ); + + expect(rendered.getByText('Test 1')).toBeInTheDocument(); + expect(rendered.getByText('Test 2')).toBeInTheDocument(); + expect(rendered.getByText('Test 3')).toBeInTheDocument(); + expect(rendered.queryByText('Test 4')).not.toBeInTheDocument(); + expect(rendered.queryByText('Test 5')).not.toBeInTheDocument(); + }); + + test('should work when using class components', () => { + const props = {}; + const Component1 = class extends React.Component<{}> { + render() { + return
Test 1
; + } + }; + const Component2 = class extends React.Component<{}> { + render() { + return
Test 2
; + } + }; + + const components: Array> = [ + createComponent(Component1, undefined, 'id-1'), + createComponent(Component2, undefined, 'id-2'), + ]; + + // Check if the limit is respected + expect(getLimitedComponentsToRender({ props, components, limit: 1 })?.length).toEqual(1); + expect(getLimitedComponentsToRender({ props, components, limit: 2 })?.length).toEqual(2); + + // Check if the right components are selected + const limitedComponents = getLimitedComponentsToRender({ props, components, limit: 1 }); + const rendered = render( + <>{limitedComponents?.map((Component, index) => )} + ); + + expect(rendered.getByText('Test 1')).toBeInTheDocument(); + expect(rendered.queryByText('Test 2')).not.toBeInTheDocument(); + }); + + test('should filter components by plugin id', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, 'plugin-id-1', 'id-1'), + createComponent(() =>
Test 2
, 'plugin-id-2', 'id-2'), + createComponent(() =>
Test 3
, 'plugin-id-3', 'id-3'), + createComponent(() =>
Test 4
, 'plugin-id-4', 'id-4'), + createComponent(() =>
Test 5
, 'plugin-id-5', 'id-5'), + ]; + + // Check if the filtering works + expect(getLimitedComponentsToRender({ props, components, pluginId: 'plugin-id-1' })?.length).toEqual(1); + expect( + getLimitedComponentsToRender({ props, components, pluginId: ['plugin-id-1', 'plugin-id-2'] })?.length + ).toEqual(2); + expect(getLimitedComponentsToRender({ props, components, pluginId: /plugin-id.*/ })?.length).toEqual(5); + + // Check if the right components are selected + const limitedComponents = getLimitedComponentsToRender({ + props, + components, + pluginId: ['plugin-id-2', 'plugin-id-3'], + }); + const rendered = render( + <>{limitedComponents?.map((Component, index) => )} + ); + + expect(rendered.getByText('Test 2')).toBeInTheDocument(); + expect(rendered.getByText('Test 3')).toBeInTheDocument(); + expect(rendered.queryByText('Test 1')).not.toBeInTheDocument(); + expect(rendered.queryByText('Test 4')).not.toBeInTheDocument(); + expect(rendered.queryByText('Test 5')).not.toBeInTheDocument(); + }); + + test('should filter components based on both limit and plugin id', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, 'plugin-id-1', 'id-1'), + createComponent(() =>
Test 2
, 'plugin-id-2', 'id-2'), + createComponent(() =>
Test 3
, 'plugin-id-3', 'id-3'), + createComponent(() =>
Test 4
, 'plugin-id-4', 'id-4'), + createComponent(() =>
Test 5
, 'plugin-id-5', 'id-5'), + ]; + + // Check if the filtering works + expect(getLimitedComponentsToRender({ props, components, limit: 1, pluginId: /plugin-id.*/ })?.length).toEqual(1); + expect(getLimitedComponentsToRender({ props, components, limit: 2, pluginId: 'plugin-id-3' })?.length).toEqual(1); + expect( + getLimitedComponentsToRender({ + props, + components, + limit: 1, + pluginId: ['plugin-id-1', 'plugin-id-2', 'plugin-id-3'], + })?.length + ).toEqual(1); + }); + }); + + describe('renderLimitedComponents()', () => { + test('should render all components if no limit is provided', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, 'plugin-id-1', 'id-1'), + createComponent(() =>
Test 2
, 'plugin-id-1', 'id-2'), + createComponent(() =>
Test 3
, 'plugin-id-2', 'id-3'), + createComponent(() =>
Test 4
, 'plugin-id-3', 'id-4'), + ]; + + const rendered = render(<>{renderLimitedComponents({ props, components })}); + + expect(rendered.getByText('Test 1')).toBeInTheDocument(); + expect(rendered.getByText('Test 2')).toBeInTheDocument(); + expect(rendered.getByText('Test 3')).toBeInTheDocument(); + expect(rendered.getByText('Test 4')).toBeInTheDocument(); + }); + + test('should limit the number of components', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, 'plugin-id-1', 'id-1'), + createComponent(() =>
Test 2
, 'plugin-id-2', 'id-2'), + createComponent(() =>
Test 3
, 'plugin-id-3', 'id-3'), + ]; + + const rendered = render(<>{renderLimitedComponents({ props, components, limit: 1 })}); + + expect(rendered.getByText('Test 1')).toBeInTheDocument(); + expect(rendered.queryByText('Test 2')).not.toBeInTheDocument(); + expect(rendered.queryByText('Test 3')).not.toBeInTheDocument(); + }); + + test('should filter components by plugin id', () => { + const props = {}; + const components: Array> = [ + createComponent(() =>
Test 1
, 'plugin-id-1', 'id-1'), + createComponent(() =>
Test 2
, 'plugin-id-2', 'id-2'), + createComponent(() =>
Test 3
, 'plugin-id-3', 'id-3'), + ]; + + const rendered = render(<>{renderLimitedComponents({ props, components, pluginId: ['plugin-id-2'] })}); + expect(rendered.getByText('Test 2')).toBeInTheDocument(); + expect(rendered.queryByText('Test 1')).not.toBeInTheDocument(); + expect(rendered.queryByText('Test 3')).not.toBeInTheDocument(); + }); + }); +}); + +function createComponent( + Implementation?: React.ComponentType, + pluginId?: string, + id?: string +): ComponentTypeWithExtensionMeta { + function ComponentWithMeta(props: Props) { + if (Implementation) { + return ; + } + + return
Test
; + } + + ComponentWithMeta.displayName = ''; + ComponentWithMeta.propTypes = {}; + ComponentWithMeta.contextTypes = {}; + ComponentWithMeta.meta = { + id: id ?? '', + pluginId: pluginId ?? '', + title: '', + description: '', + type: PluginExtensionTypes.component, + } satisfies PluginExtensionComponentMeta; + + return ComponentWithMeta; +} diff --git a/packages/grafana-runtime/src/services/pluginExtensions/utils.ts b/packages/grafana-runtime/src/services/pluginExtensions/utils.ts deleted file mode 100644 index afd08da9e49..00000000000 --- a/packages/grafana-runtime/src/services/pluginExtensions/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - type PluginExtension, - type PluginExtensionComponent, - type PluginExtensionLink, - PluginExtensionTypes, -} from '@grafana/data'; - -export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink { - if (!extension) { - return false; - } - return extension.type === PluginExtensionTypes.link && ('path' in extension || 'onClick' in extension); -} - -export function isPluginExtensionComponent( - extension: PluginExtension | undefined -): extension is PluginExtensionComponent { - if (!extension) { - return false; - } - return extension.type === PluginExtensionTypes.component && 'component' in extension; -} diff --git a/packages/grafana-runtime/src/services/pluginExtensions/utils.tsx b/packages/grafana-runtime/src/services/pluginExtensions/utils.tsx new file mode 100644 index 00000000000..3bb27160ef5 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginExtensions/utils.tsx @@ -0,0 +1,103 @@ +import React from 'react'; + +import { + ComponentTypeWithExtensionMeta, + type PluginExtension, + type PluginExtensionComponent, + type PluginExtensionLink, + PluginExtensionTypes, +} from '@grafana/data'; + +export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink { + if (!extension) { + return false; + } + return extension.type === PluginExtensionTypes.link && ('path' in extension || 'onClick' in extension); +} + +export function isPluginExtensionComponent( + extension: PluginExtension | undefined +): extension is PluginExtensionComponent { + if (!extension) { + return false; + } + return extension.type === PluginExtensionTypes.component && 'component' in extension; +} + +export function getLimitedComponentsToRender({ + props, + components, + limit, + pluginId, +}: { + props: Props; + components: Array>; + limit?: number; + pluginId?: string | string[] | RegExp; +}) { + if (!components.length) { + return null; + } + + const renderedComponents: Array> = []; + + for (const Component of components) { + const { meta } = Component; + + if (pluginId && typeof pluginId === 'string' && pluginId !== meta.pluginId) { + continue; + } + + if (pluginId && Array.isArray(pluginId) && !pluginId.includes(meta.pluginId)) { + continue; + } + + if (pluginId instanceof RegExp && !pluginId.test(meta.pluginId)) { + continue; + } + + // If no limit is provided, return all components + if (limit === undefined) { + renderedComponents.push(Component); + continue; + } + + // If a component does not render anything, do not count it in the limit + if (React.createElement(Component, props) !== null) { + renderedComponents.push(Component); + } + + // Stop if we've reached the limit + if (renderedComponents.length >= limit) { + break; + } + } + + return renderedComponents; +} + +export function renderLimitedComponents({ + props, + components, + limit, + pluginId, +}: { + props: Props; + components: Array>; + limit?: number; + pluginId?: string | string[] | RegExp; +}) { + const limitedComponents = getLimitedComponentsToRender({ props, components, limit, pluginId }); + + if (!limitedComponents?.length) { + return null; + } + + return ( + <> + {limitedComponents.map((Component) => ( + + ))} + + ); +} diff --git a/packages/grafana-runtime/src/types/i18n.ts b/packages/grafana-runtime/src/types/i18n.ts new file mode 100644 index 00000000000..31879879a5f --- /dev/null +++ b/packages/grafana-runtime/src/types/i18n.ts @@ -0,0 +1,63 @@ +/** + * Hook type for translation function that takes an ID, default message, and optional values + * @returns A function that returns the translated string + */ +type UseTranslateHook = () => (id: string, defaultMessage: string, values?: Record) => string; + +/** + * Type for children elements in Trans component + * Can be either React nodes or an object of values + */ +type TransChild = React.ReactNode | Record; + +/** + * Props interface for the Trans component used for internationalization + */ +interface TransProps { + /** + * The translation key to look up + */ + i18nKey: string; + /** + * Child elements or values to interpolate + */ + children?: TransChild | readonly TransChild[]; + /** + * React elements to use for interpolation + */ + components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement }; + /** + * Count value for pluralization + */ + count?: number; + /** + * Default text if translation is not found + */ + defaults?: string; + /** + * Namespace for the translation key + */ + ns?: string; + /** + * Whether to unescape HTML entities + */ + shouldUnescape?: boolean; + /** + * Values to interpolate into the translation + */ + values?: Record; +} + +/** + * Function declaration for the Trans component + * @param props - The TransProps object containing translation configuration + * @returns A React element with translated content + */ +declare function Trans(props: TransProps): React.ReactElement; + +/** + * Type alias for the Trans component + */ +type TransType = typeof Trans; + +export type { UseTranslateHook, TransProps, TransType }; diff --git a/packages/grafana-runtime/src/unstable.ts b/packages/grafana-runtime/src/unstable.ts index 84075dbb08a..07bb817ac5b 100644 --- a/packages/grafana-runtime/src/unstable.ts +++ b/packages/grafana-runtime/src/unstable.ts @@ -9,4 +9,5 @@ * and be subject to the standard policies */ -export { useTranslate, setUseTranslateHook } from './utils/i18n'; +export { useTranslate, setUseTranslateHook, setTransComponent, Trans } from './utils/i18n'; +export type { TransProps } from './types/i18n'; diff --git a/packages/grafana-runtime/src/utils/i18n.ts b/packages/grafana-runtime/src/utils/i18n.ts deleted file mode 100644 index f67f63316e5..00000000000 --- a/packages/grafana-runtime/src/utils/i18n.ts +++ /dev/null @@ -1,21 +0,0 @@ -type UseTranslateHook = () => (id: string, defaultMessage: string, values?: Record) => string; - -/** - * Provides a i18next-compatible translation function. - */ -export let useTranslate: UseTranslateHook = () => { - // Fallback implementation that should be overridden by setUseT - const errorMessage = 'useTranslate is not set. useTranslate must not be called before Grafana is initialized.'; - if (process.env.NODE_ENV === 'development') { - throw new Error(errorMessage); - } - - console.error(errorMessage); - return (id: string, defaultMessage: string) => { - return defaultMessage; - }; -}; - -export function setUseTranslateHook(hook: UseTranslateHook) { - useTranslate = hook; -} diff --git a/packages/grafana-runtime/src/utils/i18n.tsx b/packages/grafana-runtime/src/utils/i18n.tsx new file mode 100644 index 00000000000..30c06263dfc --- /dev/null +++ b/packages/grafana-runtime/src/utils/i18n.tsx @@ -0,0 +1,57 @@ +import { type TransProps, type TransType, type UseTranslateHook } from '../types/i18n'; + +/** + * Provides a i18next-compatible translation function. + */ +export let useTranslate: UseTranslateHook = useTranslateDefault; + +function useTranslateDefault() { + // Fallback implementation that should be overridden by setUseT + const errorMessage = 'useTranslate is not set. useTranslate must not be called before Grafana is initialized.'; + if (process.env.NODE_ENV === 'development') { + throw new Error(errorMessage); + } + + console.error(errorMessage); + return (id: string, defaultMessage: string) => { + return defaultMessage; + }; +} + +export function setUseTranslateHook(hook: UseTranslateHook) { + useTranslate = hook; +} + +let TransComponent: TransType | undefined; + +/** + * Sets the Trans component that will be used for translations throughout the application. + * This function should only be called once during application initialization. + * + * @param transComponent - The Trans component function to use for translations + * @throws {Error} If called multiple times outside of test environment + */ +export function setTransComponent(transComponent: TransType) { + // We allow overriding the trans component in tests + if (TransComponent && process.env.NODE_ENV !== 'test') { + throw new Error('setTransComponent() function should only be called once, when Grafana is starting.'); + } + + TransComponent = transComponent; +} + +/** + * A React component for handling translations with support for interpolation and pluralization. + * This component must be initialized using setTransComponent before use. + * + * @param props - The translation props including the i18nKey and any interpolation values + * @returns A React element containing the translated content + * @throws {Error} If the Trans component hasn't been initialized + */ +export function Trans(props: TransProps): React.ReactElement { + if (!TransComponent) { + throw new Error('Trans component not set. Use setTransComponent to set the Trans component.'); + } + + return ; +} diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 3a97767a218..d6b8759e8b1 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index fb399d8d9af..b8c475c5cb0 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { limit: number; diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index ef9b15eb259..a613949f576 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting { /** diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index 6eeabf6c4da..8ea837b198b 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.OptionsWithLegend, common.SingleStatBaseOptions { displayMode: common.BarGaugeDisplayMode; diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index 51ffd29995a..1ab1eeb4005 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export enum VizDisplayMode { Candles = 'candles', diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 7172fd336ec..2e70c51ccbb 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index e6b8fc13cd0..30ac8aa2af5 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface MetricStat { /** diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index 376cd0293d6..c345f24064b 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index ffc4365a8ae..14c4faac257 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index d2119d55213..7ea1d48bd5d 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index d11da7e9b28..57fa63d3a07 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index f1a444fd18b..0678c2947de 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.SingleStatBaseOptions { minVizHeight: number; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index f1960dd27a4..8258f6d0501 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index 96ef75f8618..b440943dacd 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; /** * Controls the color mode of the heatmap diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index 91e402b587f..4871a0e625b 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index a2c30353a8e..39ef7f65f58 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { dedupStrategy: common.LogsDedupStrategy; diff --git a/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts index b758575dba2..5ff339342be 100644 --- a/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { dedupStrategy: common.LogsDedupStrategy; diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index e5e064c929a..cc56fb954fa 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export enum QueryEditorMode { Builder = 'builder', diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index 36210e7648d..cbd6c366085 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index 34084472b19..b6c0e360636 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index 0f8681b7b78..e9dbbd45d5c 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index e731d7fa1fe..cac66616404 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index e589cdde3d8..f3e32f729ee 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index beb6db453da..554a2e83491 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index bc9b29ca205..fb3d375fd29 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index 9a279f1653b..342f399cd17 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -19,7 +19,7 @@ export interface TempoQuery extends common.DataQuery { exemplars?: number; filters: Array; /** - * Filters that are used to query the metrics summary + * deprecated Filters that are used to query the metrics summary */ groupBy?: Array; /** diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index a3cbd4096a9..cf1dc84ee31 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index be8096fcf67..7cb398ca8de 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index e9e79e21c63..a0cb5ed211c 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index 208d73a91c2..9e86a2ac7de 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.6.0-pre"; +export const pluginVersion = "12.0.0-pre"; export enum PointShape { Circle = 'circle', diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 846d0cabd8f..e0fdc845ec3 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -554,7 +554,7 @@ RowsLayoutRowSpec: { collapsed: bool repeat?: RowRepeatOptions conditionalRendering?: ConditionalRenderingGroupKind - layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind + layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind | RowsLayoutKind } ResponsiveGridLayoutKind: { @@ -595,7 +595,7 @@ TabsLayoutTabKind: { TabsLayoutTabSpec: { title?: string - layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind } PanelSpec: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 895014f6b6c..022ee9f3bc8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -51,49 +51,54 @@ export const defaultDashboardV2Spec = (): DashboardV2Spec => ({ variables: [], }); -// Supported dashboard elements -// |* more element types in the future -export type Element = PanelKind | LibraryPanelKind; - -export const defaultElement = (): Element => (defaultPanelKind()); - -export interface LibraryPanelKind { - kind: "LibraryPanel"; - spec: LibraryPanelSpec; +export interface AnnotationQueryKind { + kind: "AnnotationQuery"; + spec: AnnotationQuerySpec; } -export const defaultLibraryPanelKind = (): LibraryPanelKind => ({ - kind: "LibraryPanel", - spec: defaultLibraryPanelSpec(), +export const defaultAnnotationQueryKind = (): AnnotationQueryKind => ({ + kind: "AnnotationQuery", + spec: defaultAnnotationQuerySpec(), }); -export interface LibraryPanelSpec { - // Panel ID for the library panel in the dashboard - id: number; - // Title for the library panel in the dashboard - title: string; - libraryPanel: LibraryPanelRef; -} - -export const defaultLibraryPanelSpec = (): LibraryPanelSpec => ({ - id: 0, - title: "", - libraryPanel: defaultLibraryPanelRef(), -}); - -// A library panel is a reusable panel that you can use in any dashboard. -// When you make a change to a library panel, that change propagates to all instances of where the panel is used. -// Library panels streamline reuse of panels across multiple dashboards. -export interface LibraryPanelRef { - // Library panel name +export interface AnnotationQuerySpec { + datasource?: DataSourceRef; + query?: DataQueryKind; + enable: boolean; + hide: boolean; + iconColor: string; name: string; - // Library panel uid - uid: string; + builtIn?: boolean; + filter?: AnnotationPanelFilter; } -export const defaultLibraryPanelRef = (): LibraryPanelRef => ({ +export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ + enable: false, + hide: false, + iconColor: "", name: "", - uid: "", + builtIn: false, +}); + +export interface DataSourceRef { + // The plugin type-id + type?: string; + // Specific datasource instance + uid?: string; +} + +export const defaultDataSourceRef = (): DataSourceRef => ({ +}); + +export interface DataQueryKind { + // The kind of a DataQueryKind is the datasource type + kind: string; + spec: Record; +} + +export const defaultDataQueryKind = (): DataQueryKind => ({ + kind: "", + spec: {}, }); export interface AnnotationPanelFilter { @@ -115,51 +120,106 @@ export type DashboardCursorSync = "Off" | "Crosshair" | "Tooltip"; export const defaultDashboardCursorSync = (): DashboardCursorSync => ("Off"); -// Links with references to other dashboards or external resources -export interface DashboardLink { - // Title to display with the link - title: string; - // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) - // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` - type: DashboardLinkType; - // Icon name to be displayed with the link - icon: string; - // Tooltip to display when the user hovers their mouse over it - tooltip: string; - // Link URL. Only required/valid if the type is link - url?: string; - // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards - tags: string[]; - // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards - asDropdown: boolean; - // If true, the link will be opened in a new tab - targetBlank: boolean; - // If true, includes current template variables values in the link as query params - includeVars: boolean; - // If true, includes current time range in the link as query params - keepTime: boolean; +// Supported dashboard elements +// |* more element types in the future +export type Element = PanelKind | LibraryPanelKind; + +export const defaultElement = (): Element => (defaultPanelKind()); + +export interface PanelKind { + kind: "Panel"; + spec: PanelSpec; } -export const defaultDashboardLink = (): DashboardLink => ({ - title: "", - type: "link", - icon: "", - tooltip: "", - tags: [], - asDropdown: false, - targetBlank: false, - includeVars: false, - keepTime: false, +export const defaultPanelKind = (): PanelKind => ({ + kind: "Panel", + spec: defaultPanelSpec(), }); -export interface DataSourceRef { - // The plugin type-id - type?: string; - // Specific datasource instance - uid?: string; +export interface PanelSpec { + id: number; + title: string; + description: string; + links: DataLink[]; + data: QueryGroupKind; + vizConfig: VizConfigKind; + transparent?: boolean; } -export const defaultDataSourceRef = (): DataSourceRef => ({ +export const defaultPanelSpec = (): PanelSpec => ({ + id: 0, + title: "", + description: "", + links: [], + data: defaultQueryGroupKind(), + vizConfig: defaultVizConfigKind(), +}); + +export interface DataLink { + title: string; + url: string; + targetBlank?: boolean; +} + +export const defaultDataLink = (): DataLink => ({ + title: "", + url: "", +}); + +export interface QueryGroupKind { + kind: "QueryGroup"; + spec: QueryGroupSpec; +} + +export const defaultQueryGroupKind = (): QueryGroupKind => ({ + kind: "QueryGroup", + spec: defaultQueryGroupSpec(), +}); + +export interface QueryGroupSpec { + queries: PanelQueryKind[]; + transformations: TransformationKind[]; + queryOptions: QueryOptionsSpec; +} + +export const defaultQueryGroupSpec = (): QueryGroupSpec => ({ + queries: [], + transformations: [], + queryOptions: defaultQueryOptionsSpec(), +}); + +export interface PanelQueryKind { + kind: "PanelQuery"; + spec: PanelQuerySpec; +} + +export const defaultPanelQueryKind = (): PanelQueryKind => ({ + kind: "PanelQuery", + spec: defaultPanelQuerySpec(), +}); + +export interface PanelQuerySpec { + query: DataQueryKind; + datasource?: DataSourceRef; + refId: string; + hidden: boolean; +} + +export const defaultPanelQuerySpec = (): PanelQuerySpec => ({ + query: defaultDataQueryKind(), + refId: "", + hidden: false, +}); + +export interface TransformationKind { + // The kind of a TransformationKind is the transformation ID + kind: string; + spec: DataTransformerConfig; +} + +export const defaultTransformationKind = (): TransformationKind => ({ + kind: "", + spec: defaultDataTransformerConfig(), }); // Transformations allow to manipulate data returned by a query before the system applies a visualization. @@ -184,15 +244,54 @@ export const defaultDataTransformerConfig = (): DataTransformerConfig => ({ options: {}, }); -export interface DataLink { - title: string; - url: string; - targetBlank?: boolean; +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +export interface MatcherConfig { + // The matcher id. This is used to find the matcher implementation from registry. + id: string; + // The matcher options. This is specific to the matcher implementation. + options?: any; } -export const defaultDataLink = (): DataLink => ({ - title: "", - url: "", +export const defaultMatcherConfig = (): MatcherConfig => ({ + id: "", +}); + +export interface QueryOptionsSpec { + timeFrom?: string; + maxDataPoints?: number; + timeShift?: string; + queryCachingTTL?: number; + interval?: string; + cacheTimeout?: string; + hideTimeOverride?: boolean; +} + +export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ +}); + +export interface VizConfigKind { + // The kind of a VizConfigKind is the plugin ID + kind: string; + spec: VizConfigSpec; +} + +export const defaultVizConfigKind = (): VizConfigKind => ({ + kind: "", + spec: defaultVizConfigSpec(), +}); + +// --- Kinds --- +export interface VizConfigSpec { + pluginVersion: string; + options: Record; + fieldConfig: FieldConfigSource; +} + +export const defaultVizConfigSpec = (): VizConfigSpec => ({ + pluginVersion: "", + options: {}, + fieldConfig: defaultFieldConfigSource(), }); // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. @@ -272,65 +371,10 @@ export interface FieldConfig { export const defaultFieldConfig = (): FieldConfig => ({ }); -export interface DynamicConfigValue { - id: string; - value?: any; -} - -export const defaultDynamicConfigValue = (): DynamicConfigValue => ({ - id: "", -}); - -// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. -// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. -export interface MatcherConfig { - // The matcher id. This is used to find the matcher implementation from registry. - id: string; - // The matcher options. This is specific to the matcher implementation. - options?: any; -} - -export const defaultMatcherConfig = (): MatcherConfig => ({ - id: "", -}); - -export interface Threshold { - value: number; - color: string; -} - -export const defaultThreshold = (): Threshold => ({ - value: 0, - color: "", -}); - -export type ThresholdsMode = "absolute" | "percentage"; - -export const defaultThresholdsMode = (): ThresholdsMode => ("absolute"); - -export interface ThresholdsConfig { - mode: ThresholdsMode; - steps: Threshold[]; -} - -export const defaultThresholdsConfig = (): ThresholdsConfig => ({ - mode: "absolute", - steps: [], -}); - export type ValueMapping = ValueMap | RangeMap | RegexMap | SpecialValueMap; export const defaultValueMapping = (): ValueMapping => (defaultValueMap()); -// Supported value mapping types -// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. -// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. -// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. -// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. -export type MappingType = "value" | "range" | "regex" | "special"; - -export const defaultMappingType = (): MappingType => ("value"); - // Maps text values to a color or different display text and color. // For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. export interface ValueMap { @@ -344,6 +388,21 @@ export const defaultValueMap = (): ValueMap => ({ options: {}, }); +// Result used as replacement with text and color when the value matches +export interface ValueMappingResult { + // Text to display when the value matches + text?: string; + // Text to use when the value matches + color?: string; + // Icon to display when the value matches. Only specific visualizations. + icon?: string; + // Position in the mapping array. Only used internally. + index?: number; +} + +export const defaultValueMappingResult = (): ValueMappingResult => ({ +}); + // Maps numerical ranges to a display text and color. // For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. export interface RangeMap { @@ -415,19 +474,42 @@ export type SpecialValueMatch = "true" | "false" | "null" | "nan" | "null+nan" | export const defaultSpecialValueMatch = (): SpecialValueMatch => ("true"); -// Result used as replacement with text and color when the value matches -export interface ValueMappingResult { - // Text to display when the value matches - text?: string; - // Text to use when the value matches - color?: string; - // Icon to display when the value matches. Only specific visualizations. - icon?: string; - // Position in the mapping array. Only used internally. - index?: number; +export interface ThresholdsConfig { + mode: ThresholdsMode; + steps: Threshold[]; } -export const defaultValueMappingResult = (): ValueMappingResult => ({ +export const defaultThresholdsConfig = (): ThresholdsConfig => ({ + mode: "absolute", + steps: [], +}); + +export type ThresholdsMode = "absolute" | "percentage"; + +export const defaultThresholdsMode = (): ThresholdsMode => ("absolute"); + +export interface Threshold { + value: number; + color: string; +} + +export const defaultThreshold = (): Threshold => ({ + value: 0, + color: "", +}); + +// Map a field to a color. +export interface FieldColor { + // The main color scheme mode. + mode: FieldColorModeId; + // The fixed color value for fixed or shades color modes. + fixedColor?: string; + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: FieldColorSeriesByMode; +} + +export const defaultFieldColor = (): FieldColor => ({ + mode: "thresholds", }); // Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. @@ -457,268 +539,80 @@ export type FieldColorSeriesByMode = "min" | "max" | "last"; export const defaultFieldColorSeriesByMode = (): FieldColorSeriesByMode => ("min"); -// Map a field to a color. -export interface FieldColor { - // The main color scheme mode. - mode: FieldColorModeId; - // The fixed color value for fixed or shades color modes. - fixedColor?: string; - // Some visualizations need to know how to assign a series color from by value color schemes. - seriesBy?: FieldColorSeriesByMode; +export interface DynamicConfigValue { + id: string; + value?: any; } -export const defaultFieldColor = (): FieldColor => ({ - mode: "thresholds", +export const defaultDynamicConfigValue = (): DynamicConfigValue => ({ + id: "", }); -// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) -export type DashboardLinkType = "link" | "dashboards"; - -export const defaultDashboardLinkType = (): DashboardLinkType => ("link"); - -// --- Common types --- -export interface Kind { - kind: string; - spec: any; - metadata?: any; +export interface LibraryPanelKind { + kind: "LibraryPanel"; + spec: LibraryPanelSpec; } -export const defaultKind = (): Kind => ({ - kind: "", - spec: {}, +export const defaultLibraryPanelKind = (): LibraryPanelKind => ({ + kind: "LibraryPanel", + spec: defaultLibraryPanelSpec(), }); -// --- Kinds --- -export interface VizConfigSpec { - pluginVersion: string; - options: Record; - fieldConfig: FieldConfigSource; +export interface LibraryPanelSpec { + // Panel ID for the library panel in the dashboard + id: number; + // Title for the library panel in the dashboard + title: string; + libraryPanel: LibraryPanelRef; } -export const defaultVizConfigSpec = (): VizConfigSpec => ({ - pluginVersion: "", - options: {}, - fieldConfig: defaultFieldConfigSource(), +export const defaultLibraryPanelSpec = (): LibraryPanelSpec => ({ + id: 0, + title: "", + libraryPanel: defaultLibraryPanelRef(), }); -export interface VizConfigKind { - // The kind of a VizConfigKind is the plugin ID - kind: string; - spec: VizConfigSpec; -} - -export const defaultVizConfigKind = (): VizConfigKind => ({ - kind: "", - spec: defaultVizConfigSpec(), -}); - -export interface AnnotationQuerySpec { - datasource?: DataSourceRef; - query?: DataQueryKind; - enable: boolean; - hide: boolean; - iconColor: string; +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +export interface LibraryPanelRef { + // Library panel name name: string; - builtIn?: boolean; - filter?: AnnotationPanelFilter; + // Library panel uid + uid: string; } -export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ - enable: false, - hide: false, - iconColor: "", +export const defaultLibraryPanelRef = (): LibraryPanelRef => ({ name: "", - builtIn: false, + uid: "", }); -export interface AnnotationQueryKind { - kind: "AnnotationQuery"; - spec: AnnotationQuerySpec; +export interface GridLayoutKind { + kind: "GridLayout"; + spec: GridLayoutSpec; } -export const defaultAnnotationQueryKind = (): AnnotationQueryKind => ({ - kind: "AnnotationQuery", - spec: defaultAnnotationQuerySpec(), +export const defaultGridLayoutKind = (): GridLayoutKind => ({ + kind: "GridLayout", + spec: defaultGridLayoutSpec(), }); -export interface QueryOptionsSpec { - timeFrom?: string; - maxDataPoints?: number; - timeShift?: string; - queryCachingTTL?: number; - interval?: string; - cacheTimeout?: string; - hideTimeOverride?: boolean; +export interface GridLayoutSpec { + items: (GridLayoutItemKind | GridLayoutRowKind)[]; } -export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ +export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ + items: [], }); -export interface DataQueryKind { - // The kind of a DataQueryKind is the datasource type - kind: string; - spec: Record; +export interface GridLayoutItemKind { + kind: "GridLayoutItem"; + spec: GridLayoutItemSpec; } -export const defaultDataQueryKind = (): DataQueryKind => ({ - kind: "", - spec: {}, -}); - -export interface PanelQuerySpec { - query: DataQueryKind; - datasource?: DataSourceRef; - refId: string; - hidden: boolean; -} - -export const defaultPanelQuerySpec = (): PanelQuerySpec => ({ - query: defaultDataQueryKind(), - refId: "", - hidden: false, -}); - -export interface PanelQueryKind { - kind: "PanelQuery"; - spec: PanelQuerySpec; -} - -export const defaultPanelQueryKind = (): PanelQueryKind => ({ - kind: "PanelQuery", - spec: defaultPanelQuerySpec(), -}); - -export interface TransformationKind { - // The kind of a TransformationKind is the transformation ID - kind: string; - spec: DataTransformerConfig; -} - -export const defaultTransformationKind = (): TransformationKind => ({ - kind: "", - spec: defaultDataTransformerConfig(), -}); - -export interface QueryGroupSpec { - queries: PanelQueryKind[]; - transformations: TransformationKind[]; - queryOptions: QueryOptionsSpec; -} - -export const defaultQueryGroupSpec = (): QueryGroupSpec => ({ - queries: [], - transformations: [], - queryOptions: defaultQueryOptionsSpec(), -}); - -export interface QueryGroupKind { - kind: "QueryGroup"; - spec: QueryGroupSpec; -} - -export const defaultQueryGroupKind = (): QueryGroupKind => ({ - kind: "QueryGroup", - spec: defaultQueryGroupSpec(), -}); - -export interface TimeRangeOption { - display: string; - from: string; - to: string; -} - -export const defaultTimeRangeOption = (): TimeRangeOption => ({ - display: "Last 6 hours", - from: "now-6h", - to: "now", -}); - -// Time configuration -// It defines the default time config for the time picker, the refresh picker for the specific dashboard. -export interface TimeSettingsSpec { - // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". - timezone?: string; - // Start time range for dashboard. - // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". - from: string; - // End time range for dashboard. - // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". - to: string; - // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". - // v1: refresh - autoRefresh: string; - // Interval options available in the refresh picker dropdown. - // v1: timepicker.refresh_intervals - autoRefreshIntervals: string[]; - // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - // v1: timepicker.quick_ranges , not exposed in the UI - quickRanges?: TimeRangeOption[]; - // Whether timepicker is visible or not. - // v1: timepicker.hidden - hideTimepicker: boolean; - // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". - weekStart?: "saturday" | "monday" | "sunday"; - // The month that the fiscal year starts on. 0 = January, 11 = December - fiscalYearStartMonth: number; - // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. - // v1: timepicker.nowDelay - nowDelay?: string; -} - -export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({ - timezone: "browser", - from: "now-6h", - to: "now", - autoRefresh: "", - autoRefreshIntervals: [ -"5s", -"10s", -"30s", -"1m", -"5m", -"15m", -"30m", -"1h", -"2h", -"1d", -], - hideTimepicker: false, - fiscalYearStartMonth: 0, -}); - -// other repeat modes will be added in the future: label, frame -export const RepeatMode = "variable"; - -export interface RepeatOptions { - mode: "variable"; - value: string; - direction?: "h" | "v"; - maxPerRow?: number; -} - -export const defaultRepeatOptions = (): RepeatOptions => ({ - mode: RepeatMode, - value: "", -}); - -export interface RowRepeatOptions { - mode: "variable"; - value: string; -} - -export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ - mode: RepeatMode, - value: "", -}); - -export interface ResponsiveGridRepeatOptions { - mode: "variable"; - value: string; -} - -export const defaultResponsiveGridRepeatOptions = (): ResponsiveGridRepeatOptions => ({ - mode: RepeatMode, - value: "", +export const defaultGridLayoutItemKind = (): GridLayoutItemKind => ({ + kind: "GridLayoutItem", + spec: defaultGridLayoutItemSpec(), }); export interface GridLayoutItemSpec { @@ -739,16 +633,31 @@ export const defaultGridLayoutItemSpec = (): GridLayoutItemSpec => ({ element: defaultElementReference(), }); -export interface GridLayoutItemKind { - kind: "GridLayoutItem"; - spec: GridLayoutItemSpec; +export interface ElementReference { + kind: "ElementReference"; + name: string; } -export const defaultGridLayoutItemKind = (): GridLayoutItemKind => ({ - kind: "GridLayoutItem", - spec: defaultGridLayoutItemSpec(), +export const defaultElementReference = (): ElementReference => ({ + kind: "ElementReference", + name: "", }); +export interface RepeatOptions { + mode: "variable"; + value: string; + direction?: "h" | "v"; + maxPerRow?: number; +} + +export const defaultRepeatOptions = (): RepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +// other repeat modes will be added in the future: label, frame +export const RepeatMode = "variable"; + export interface GridLayoutRowKind { kind: "GridLayoutRow"; spec: GridLayoutRowSpec; @@ -775,22 +684,14 @@ export const defaultGridLayoutRowSpec = (): GridLayoutRowSpec => ({ elements: [], }); -export interface GridLayoutSpec { - items: (GridLayoutItemKind | GridLayoutRowKind)[]; +export interface RowRepeatOptions { + mode: "variable"; + value: string; } -export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ - items: [], -}); - -export interface GridLayoutKind { - kind: "GridLayout"; - spec: GridLayoutSpec; -} - -export const defaultGridLayoutKind = (): GridLayoutKind => ({ - kind: "GridLayout", - spec: defaultGridLayoutSpec(), +export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ + mode: RepeatMode, + value: "", }); export interface RowsLayoutKind { @@ -826,7 +727,7 @@ export interface RowsLayoutRowSpec { collapsed: boolean; repeat?: RowRepeatOptions; conditionalRendering?: ConditionalRenderingGroupKind; - layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind; + layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind | RowsLayoutKind; } export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({ @@ -834,575 +735,6 @@ export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({ layout: defaultGridLayoutKind(), }); -export interface ResponsiveGridLayoutKind { - kind: "ResponsiveGridLayout"; - spec: ResponsiveGridLayoutSpec; -} - -export const defaultResponsiveGridLayoutKind = (): ResponsiveGridLayoutKind => ({ - kind: "ResponsiveGridLayout", - spec: defaultResponsiveGridLayoutSpec(), -}); - -export interface ResponsiveGridLayoutSpec { - row: string; - col: string; - items: ResponsiveGridLayoutItemKind[]; -} - -export const defaultResponsiveGridLayoutSpec = (): ResponsiveGridLayoutSpec => ({ - row: "", - col: "", - items: [], -}); - -export interface ResponsiveGridLayoutItemKind { - kind: "ResponsiveGridLayoutItem"; - spec: ResponsiveGridLayoutItemSpec; -} - -export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemKind => ({ - kind: "ResponsiveGridLayoutItem", - spec: defaultResponsiveGridLayoutItemSpec(), -}); - -export interface ResponsiveGridLayoutItemSpec { - element: ElementReference; - repeat?: ResponsiveGridRepeatOptions; - conditionalRendering?: ConditionalRenderingGroupKind; -} - -export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({ - element: defaultElementReference(), -}); - -export interface TabsLayoutKind { - kind: "TabsLayout"; - spec: TabsLayoutSpec; -} - -export const defaultTabsLayoutKind = (): TabsLayoutKind => ({ - kind: "TabsLayout", - spec: defaultTabsLayoutSpec(), -}); - -export interface TabsLayoutSpec { - tabs: TabsLayoutTabKind[]; -} - -export const defaultTabsLayoutSpec = (): TabsLayoutSpec => ({ - tabs: [], -}); - -export interface TabsLayoutTabKind { - kind: "TabsLayoutTab"; - spec: TabsLayoutTabSpec; -} - -export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({ - kind: "TabsLayoutTab", - spec: defaultTabsLayoutTabSpec(), -}); - -export interface TabsLayoutTabSpec { - title?: string; - layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind; -} - -export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({ - layout: defaultGridLayoutKind(), -}); - -export interface PanelSpec { - id: number; - title: string; - description: string; - links: DataLink[]; - data: QueryGroupKind; - vizConfig: VizConfigKind; - transparent?: boolean; -} - -export const defaultPanelSpec = (): PanelSpec => ({ - id: 0, - title: "", - description: "", - links: [], - data: defaultQueryGroupKind(), - vizConfig: defaultVizConfigKind(), -}); - -export interface PanelKind { - kind: "Panel"; - spec: PanelSpec; -} - -export const defaultPanelKind = (): PanelKind => ({ - kind: "Panel", - spec: defaultPanelSpec(), -}); - -export interface ElementReference { - kind: "ElementReference"; - name: string; -} - -export const defaultElementReference = (): ElementReference => ({ - kind: "ElementReference", - name: "", -}); - -// Variable types -export type VariableValue = VariableValueSingle | VariableValueSingle[]; - -export const defaultVariableValue = (): VariableValue => (defaultVariableValueSingle()); - -export type VariableValueSingle = string | boolean | number | CustomVariableValue; - -export const defaultVariableValueSingle = (): VariableValueSingle => (""); - -// Custom formatter variable -export interface CustomFormatterVariable { - name: string; - type: VariableType; - multi: boolean; - includeAll: boolean; -} - -export const defaultCustomFormatterVariable = (): CustomFormatterVariable => ({ - name: "", - type: "query", - multi: false, - includeAll: false, -}); - -// Custom variable value -export interface CustomVariableValue { - // The format name or function used in the expression - formatter: string | VariableCustomFormatterFn; -} - -export const defaultCustomVariableValue = (): CustomVariableValue => ({ - formatter: "", -}); - -// Custom formatter function -export interface VariableCustomFormatterFn { - value: any; - legacyVariableModel: { - name: string; - type: VariableType; - multi: boolean; - includeAll: boolean; - }; - legacyDefaultFormatter?: VariableCustomFormatterFn; -} - -export const defaultVariableCustomFormatterFn = (): VariableCustomFormatterFn => ({ - value: {}, - legacyVariableModel: { - name: "", - type: "query", - multi: false, - includeAll: false, -}, -}); - -// Dashboard variable type -// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. -// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). -// `constant`: Define a hidden constant. -// `datasource`: Quickly change the data source for an entire dashboard. -// `interval`: Interval variables represent time spans. -// `textbox`: Display a free text input field with an optional default value. -// `custom`: Define the variable options manually using a comma-separated list. -// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables -export type VariableType = "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | "system" | "snapshot"; - -export const defaultVariableType = (): VariableType => ("query"); - -export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; - -export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); - -// Sort variable options -// Accepted values are: -// `disabled`: No sorting -// `alphabeticalAsc`: Alphabetical ASC -// `alphabeticalDesc`: Alphabetical DESC -// `numericalAsc`: Numerical ASC -// `numericalDesc`: Numerical DESC -// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC -// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC -// `naturalAsc`: Natural ASC -// `naturalDesc`: Natural DESC -// VariableSort enum with default value -export type VariableSort = "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc"; - -export const defaultVariableSort = (): VariableSort => ("disabled"); - -// Options to config when to refresh a variable -// `never`: Never refresh the variable -// `onDashboardLoad`: Queries the data source every time the dashboard loads. -// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. -export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"; - -export const defaultVariableRefresh = (): VariableRefresh => ("never"); - -// Determine if the variable shows on dashboard -// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). -export type VariableHide = "dontHide" | "hideLabel" | "hideVariable"; - -export const defaultVariableHide = (): VariableHide => ("dontHide"); - -// FIXME: should we introduce this? --- Variable value option -export interface VariableValueOption { - label: string; - value: VariableValueSingle; - group?: string; -} - -export const defaultVariableValueOption = (): VariableValueOption => ({ - label: "", - value: defaultVariableValueSingle(), -}); - -// Variable option specification -export interface VariableOption { - // Whether the option is selected or not - selected?: boolean; - // Text to be displayed for the option - text: string | string[]; - // Value of the option - value: string | string[]; -} - -export const defaultVariableOption = (): VariableOption => ({ - text: "", - value: "", -}); - -// Query variable specification -export interface QueryVariableSpec { - name: string; - current: VariableOption; - label?: string; - hide: VariableHide; - refresh: VariableRefresh; - skipUrlSync: boolean; - description?: string; - datasource?: DataSourceRef; - query: DataQueryKind; - regex: string; - sort: VariableSort; - definition?: string; - options: VariableOption[]; - multi: boolean; - includeAll: boolean; - allValue?: string; - placeholder?: string; -} - -export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ - name: "", - current: { text: "", value: "", }, - hide: "dontHide", - refresh: "never", - skipUrlSync: false, - query: defaultDataQueryKind(), - regex: "", - sort: "disabled", - options: [], - multi: false, - includeAll: false, -}); - -// Query variable kind -export interface QueryVariableKind { - kind: "QueryVariable"; - spec: QueryVariableSpec; -} - -export const defaultQueryVariableKind = (): QueryVariableKind => ({ - kind: "QueryVariable", - spec: defaultQueryVariableSpec(), -}); - -// Text variable specification -export interface TextVariableSpec { - name: string; - current: VariableOption; - query: string; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultTextVariableSpec = (): TextVariableSpec => ({ - name: "", - current: { text: "", value: "", }, - query: "", - hide: "dontHide", - skipUrlSync: false, -}); - -// Text variable kind -export interface TextVariableKind { - kind: "TextVariable"; - spec: TextVariableSpec; -} - -export const defaultTextVariableKind = (): TextVariableKind => ({ - kind: "TextVariable", - spec: defaultTextVariableSpec(), -}); - -// Constant variable specification -export interface ConstantVariableSpec { - name: string; - query: string; - current: VariableOption; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({ - name: "", - query: "", - current: { text: "", value: "", }, - hide: "dontHide", - skipUrlSync: false, -}); - -// Constant variable kind -export interface ConstantVariableKind { - kind: "ConstantVariable"; - spec: ConstantVariableSpec; -} - -export const defaultConstantVariableKind = (): ConstantVariableKind => ({ - kind: "ConstantVariable", - spec: defaultConstantVariableSpec(), -}); - -// Datasource variable specification -export interface DatasourceVariableSpec { - name: string; - pluginId: string; - refresh: VariableRefresh; - regex: string; - current: VariableOption; - options: VariableOption[]; - multi: boolean; - includeAll: boolean; - allValue?: string; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({ - name: "", - pluginId: "", - refresh: "never", - regex: "", - current: { text: "", value: "", }, - options: [], - multi: false, - includeAll: false, - hide: "dontHide", - skipUrlSync: false, -}); - -// Datasource variable kind -export interface DatasourceVariableKind { - kind: "DatasourceVariable"; - spec: DatasourceVariableSpec; -} - -export const defaultDatasourceVariableKind = (): DatasourceVariableKind => ({ - kind: "DatasourceVariable", - spec: defaultDatasourceVariableSpec(), -}); - -// Interval variable specification -export interface IntervalVariableSpec { - name: string; - query: string; - current: VariableOption; - options: VariableOption[]; - auto: boolean; - auto_min: string; - auto_count: number; - refresh: VariableRefresh; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ - name: "", - query: "", - current: { text: "", value: "", }, - options: [], - auto: false, - auto_min: "", - auto_count: 0, - refresh: "never", - hide: "dontHide", - skipUrlSync: false, -}); - -// Interval variable kind -export interface IntervalVariableKind { - kind: "IntervalVariable"; - spec: IntervalVariableSpec; -} - -export const defaultIntervalVariableKind = (): IntervalVariableKind => ({ - kind: "IntervalVariable", - spec: defaultIntervalVariableSpec(), -}); - -// Custom variable specification -export interface CustomVariableSpec { - name: string; - query: string; - current: VariableOption; - options: VariableOption[]; - multi: boolean; - includeAll: boolean; - allValue?: string; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ - name: "", - query: "", - current: defaultVariableOption(), - options: [], - multi: false, - includeAll: false, - hide: "dontHide", - skipUrlSync: false, -}); - -// Custom variable kind -export interface CustomVariableKind { - kind: "CustomVariable"; - spec: CustomVariableSpec; -} - -export const defaultCustomVariableKind = (): CustomVariableKind => ({ - kind: "CustomVariable", - spec: defaultCustomVariableSpec(), -}); - -// GroupBy variable specification -export interface GroupByVariableSpec { - name: string; - datasource?: DataSourceRef; - current: VariableOption; - options: VariableOption[]; - multi: boolean; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ - name: "", - current: { text: "", value: "", }, - options: [], - multi: false, - hide: "dontHide", - skipUrlSync: false, -}); - -// Group variable kind -export interface GroupByVariableKind { - kind: "GroupByVariable"; - spec: GroupByVariableSpec; -} - -export const defaultGroupByVariableKind = (): GroupByVariableKind => ({ - kind: "GroupByVariable", - spec: defaultGroupByVariableSpec(), -}); - -// Adhoc variable specification -export interface AdhocVariableSpec { - name: string; - datasource?: DataSourceRef; - baseFilters: AdHocFilterWithLabels[]; - filters: AdHocFilterWithLabels[]; - defaultKeys: MetricFindValue[]; - label?: string; - hide: VariableHide; - skipUrlSync: boolean; - description?: string; -} - -export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({ - name: "", - baseFilters: [], - filters: [], - defaultKeys: [], - hide: "dontHide", - skipUrlSync: false, -}); - -// Define the MetricFindValue type -export interface MetricFindValue { - text: string; - value?: string | number; - group?: string; - expandable?: boolean; -} - -export const defaultMetricFindValue = (): MetricFindValue => ({ - text: "", -}); - -// Define the AdHocFilterWithLabels type -export interface AdHocFilterWithLabels { - key: string; - operator: string; - value: string; - values?: string[]; - keyLabel?: string; - valueLabels?: string[]; - forceEdit?: boolean; - // @deprecated - condition?: string; -} - -export const defaultAdHocFilterWithLabels = (): AdHocFilterWithLabels => ({ - key: "", - operator: "", - value: "", -}); - -// Adhoc variable kind -export interface AdhocVariableKind { - kind: "AdhocVariable"; - spec: AdhocVariableSpec; -} - -export const defaultAdhocVariableKind = (): AdhocVariableKind => ({ - kind: "AdhocVariable", - spec: defaultAdhocVariableSpec(), -}); - export interface ConditionalRenderingGroupKind { kind: "ConditionalRenderingGroup"; spec: ConditionalRenderingGroupSpec; @@ -1481,3 +813,671 @@ export const defaultConditionalRenderingTimeIntervalSpec = (): ConditionalRender value: "", }); +export interface ResponsiveGridLayoutKind { + kind: "ResponsiveGridLayout"; + spec: ResponsiveGridLayoutSpec; +} + +export const defaultResponsiveGridLayoutKind = (): ResponsiveGridLayoutKind => ({ + kind: "ResponsiveGridLayout", + spec: defaultResponsiveGridLayoutSpec(), +}); + +export interface ResponsiveGridLayoutSpec { + row: string; + col: string; + items: ResponsiveGridLayoutItemKind[]; +} + +export const defaultResponsiveGridLayoutSpec = (): ResponsiveGridLayoutSpec => ({ + row: "", + col: "", + items: [], +}); + +export interface ResponsiveGridLayoutItemKind { + kind: "ResponsiveGridLayoutItem"; + spec: ResponsiveGridLayoutItemSpec; +} + +export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemKind => ({ + kind: "ResponsiveGridLayoutItem", + spec: defaultResponsiveGridLayoutItemSpec(), +}); + +export interface ResponsiveGridLayoutItemSpec { + element: ElementReference; + repeat?: ResponsiveGridRepeatOptions; + conditionalRendering?: ConditionalRenderingGroupKind; +} + +export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({ + element: defaultElementReference(), +}); + +export interface ResponsiveGridRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultResponsiveGridRepeatOptions = (): ResponsiveGridRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +export interface TabsLayoutKind { + kind: "TabsLayout"; + spec: TabsLayoutSpec; +} + +export const defaultTabsLayoutKind = (): TabsLayoutKind => ({ + kind: "TabsLayout", + spec: defaultTabsLayoutSpec(), +}); + +export interface TabsLayoutSpec { + tabs: TabsLayoutTabKind[]; +} + +export const defaultTabsLayoutSpec = (): TabsLayoutSpec => ({ + tabs: [], +}); + +export interface TabsLayoutTabKind { + kind: "TabsLayoutTab"; + spec: TabsLayoutTabSpec; +} + +export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({ + kind: "TabsLayoutTab", + spec: defaultTabsLayoutTabSpec(), +}); + +export interface TabsLayoutTabSpec { + title?: string; + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind; +} + +export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({ + layout: defaultGridLayoutKind(), +}); + +// Links with references to other dashboards or external resources +export interface DashboardLink { + // Title to display with the link + title: string; + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + type: DashboardLinkType; + // Icon name to be displayed with the link + icon: string; + // Tooltip to display when the user hovers their mouse over it + tooltip: string; + // Link URL. Only required/valid if the type is link + url?: string; + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: string[]; + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: boolean; + // If true, the link will be opened in a new tab + targetBlank: boolean; + // If true, includes current template variables values in the link as query params + includeVars: boolean; + // If true, includes current time range in the link as query params + keepTime: boolean; +} + +export const defaultDashboardLink = (): DashboardLink => ({ + title: "", + type: "link", + icon: "", + tooltip: "", + tags: [], + asDropdown: false, + targetBlank: false, + includeVars: false, + keepTime: false, +}); + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +export type DashboardLinkType = "link" | "dashboards"; + +export const defaultDashboardLinkType = (): DashboardLinkType => ("link"); + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +export interface TimeSettingsSpec { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string; + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + from: string; + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + to: string; + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + // v1: refresh + autoRefresh: string; + // Interval options available in the refresh picker dropdown. + // v1: timepicker.refresh_intervals + autoRefreshIntervals: string[]; + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + // v1: timepicker.quick_ranges , not exposed in the UI + quickRanges?: TimeRangeOption[]; + // Whether timepicker is visible or not. + // v1: timepicker.hidden + hideTimepicker: boolean; + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: "saturday" | "monday" | "sunday"; + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth: number; + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + // v1: timepicker.nowDelay + nowDelay?: string; +} + +export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({ + timezone: "browser", + from: "now-6h", + to: "now", + autoRefresh: "", + autoRefreshIntervals: [ +"5s", +"10s", +"30s", +"1m", +"5m", +"15m", +"30m", +"1h", +"2h", +"1d", +], + hideTimepicker: false, + fiscalYearStartMonth: 0, +}); + +export interface TimeRangeOption { + display: string; + from: string; + to: string; +} + +export const defaultTimeRangeOption = (): TimeRangeOption => ({ + display: "Last 6 hours", + from: "now-6h", + to: "now", +}); + +export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; + +export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); + +// Query variable kind +export interface QueryVariableKind { + kind: "QueryVariable"; + spec: QueryVariableSpec; +} + +export const defaultQueryVariableKind = (): QueryVariableKind => ({ + kind: "QueryVariable", + spec: defaultQueryVariableSpec(), +}); + +// Query variable specification +export interface QueryVariableSpec { + name: string; + current: VariableOption; + label?: string; + hide: VariableHide; + refresh: VariableRefresh; + skipUrlSync: boolean; + description?: string; + datasource?: DataSourceRef; + query: DataQueryKind; + regex: string; + sort: VariableSort; + definition?: string; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + placeholder?: string; +} + +export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + hide: "dontHide", + refresh: "never", + skipUrlSync: false, + query: defaultDataQueryKind(), + regex: "", + sort: "disabled", + options: [], + multi: false, + includeAll: false, +}); + +// Variable option specification +export interface VariableOption { + // Whether the option is selected or not + selected?: boolean; + // Text to be displayed for the option + text: string | string[]; + // Value of the option + value: string | string[]; +} + +export const defaultVariableOption = (): VariableOption => ({ + text: "", + value: "", +}); + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +export type VariableHide = "dontHide" | "hideLabel" | "hideVariable"; + +export const defaultVariableHide = (): VariableHide => ("dontHide"); + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"; + +export const defaultVariableRefresh = (): VariableRefresh => ("never"); + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +export type VariableSort = "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc"; + +export const defaultVariableSort = (): VariableSort => ("disabled"); + +// Text variable kind +export interface TextVariableKind { + kind: "TextVariable"; + spec: TextVariableSpec; +} + +export const defaultTextVariableKind = (): TextVariableKind => ({ + kind: "TextVariable", + spec: defaultTextVariableSpec(), +}); + +// Text variable specification +export interface TextVariableSpec { + name: string; + current: VariableOption; + query: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultTextVariableSpec = (): TextVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + query: "", + hide: "dontHide", + skipUrlSync: false, +}); + +// Constant variable kind +export interface ConstantVariableKind { + kind: "ConstantVariable"; + spec: ConstantVariableSpec; +} + +export const defaultConstantVariableKind = (): ConstantVariableKind => ({ + kind: "ConstantVariable", + spec: defaultConstantVariableSpec(), +}); + +// Constant variable specification +export interface ConstantVariableSpec { + name: string; + query: string; + current: VariableOption; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({ + name: "", + query: "", + current: { text: "", value: "", }, + hide: "dontHide", + skipUrlSync: false, +}); + +// Datasource variable kind +export interface DatasourceVariableKind { + kind: "DatasourceVariable"; + spec: DatasourceVariableSpec; +} + +export const defaultDatasourceVariableKind = (): DatasourceVariableKind => ({ + kind: "DatasourceVariable", + spec: defaultDatasourceVariableSpec(), +}); + +// Datasource variable specification +export interface DatasourceVariableSpec { + name: string; + pluginId: string; + refresh: VariableRefresh; + regex: string; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({ + name: "", + pluginId: "", + refresh: "never", + regex: "", + current: { text: "", value: "", }, + options: [], + multi: false, + includeAll: false, + hide: "dontHide", + skipUrlSync: false, +}); + +// Interval variable kind +export interface IntervalVariableKind { + kind: "IntervalVariable"; + spec: IntervalVariableSpec; +} + +export const defaultIntervalVariableKind = (): IntervalVariableKind => ({ + kind: "IntervalVariable", + spec: defaultIntervalVariableSpec(), +}); + +// Interval variable specification +export interface IntervalVariableSpec { + name: string; + query: string; + current: VariableOption; + options: VariableOption[]; + auto: boolean; + auto_min: string; + auto_count: number; + refresh: VariableRefresh; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ + name: "", + query: "", + current: { text: "", value: "", }, + options: [], + auto: false, + auto_min: "", + auto_count: 0, + refresh: "never", + hide: "dontHide", + skipUrlSync: false, +}); + +// Custom variable kind +export interface CustomVariableKind { + kind: "CustomVariable"; + spec: CustomVariableSpec; +} + +export const defaultCustomVariableKind = (): CustomVariableKind => ({ + kind: "CustomVariable", + spec: defaultCustomVariableSpec(), +}); + +// Custom variable specification +export interface CustomVariableSpec { + name: string; + query: string; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ + name: "", + query: "", + current: defaultVariableOption(), + options: [], + multi: false, + includeAll: false, + hide: "dontHide", + skipUrlSync: false, +}); + +// Group variable kind +export interface GroupByVariableKind { + kind: "GroupByVariable"; + spec: GroupByVariableSpec; +} + +export const defaultGroupByVariableKind = (): GroupByVariableKind => ({ + kind: "GroupByVariable", + spec: defaultGroupByVariableSpec(), +}); + +// GroupBy variable specification +export interface GroupByVariableSpec { + name: string; + datasource?: DataSourceRef; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + options: [], + multi: false, + hide: "dontHide", + skipUrlSync: false, +}); + +// Adhoc variable kind +export interface AdhocVariableKind { + kind: "AdhocVariable"; + spec: AdhocVariableSpec; +} + +export const defaultAdhocVariableKind = (): AdhocVariableKind => ({ + kind: "AdhocVariable", + spec: defaultAdhocVariableSpec(), +}); + +// Adhoc variable specification +export interface AdhocVariableSpec { + name: string; + datasource?: DataSourceRef; + baseFilters: AdHocFilterWithLabels[]; + filters: AdHocFilterWithLabels[]; + defaultKeys: MetricFindValue[]; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({ + name: "", + baseFilters: [], + filters: [], + defaultKeys: [], + hide: "dontHide", + skipUrlSync: false, +}); + +// Define the AdHocFilterWithLabels type +export interface AdHocFilterWithLabels { + key: string; + operator: string; + value: string; + values?: string[]; + keyLabel?: string; + valueLabels?: string[]; + forceEdit?: boolean; + // @deprecated + condition?: string; +} + +export const defaultAdHocFilterWithLabels = (): AdHocFilterWithLabels => ({ + key: "", + operator: "", + value: "", +}); + +// Define the MetricFindValue type +export interface MetricFindValue { + text: string; + value?: string | number; + group?: string; + expandable?: boolean; +} + +export const defaultMetricFindValue = (): MetricFindValue => ({ + text: "", +}); + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +export type MappingType = "value" | "range" | "regex" | "special"; + +export const defaultMappingType = (): MappingType => ("value"); + +// --- Common types --- +export interface Kind { + kind: string; + spec: any; + metadata?: any; +} + +export const defaultKind = (): Kind => ({ + kind: "", + spec: {}, +}); + +// Variable types +export type VariableValue = VariableValueSingle | VariableValueSingle[]; + +export const defaultVariableValue = (): VariableValue => (defaultVariableValueSingle()); + +export type VariableValueSingle = string | boolean | number | CustomVariableValue; + +export const defaultVariableValueSingle = (): VariableValueSingle => (""); + +// Custom variable value +export interface CustomVariableValue { + // The format name or function used in the expression + formatter: string | VariableCustomFormatterFn; +} + +export const defaultCustomVariableValue = (): CustomVariableValue => ({ + formatter: "", +}); + +// Custom formatter function +export interface VariableCustomFormatterFn { + value: any; + legacyVariableModel: { + name: string; + type: VariableType; + multi: boolean; + includeAll: boolean; + }; + legacyDefaultFormatter?: VariableCustomFormatterFn; +} + +export const defaultVariableCustomFormatterFn = (): VariableCustomFormatterFn => ({ + value: {}, + legacyVariableModel: { + name: "", + type: "query", + multi: false, + includeAll: false, +}, +}); + +// Dashboard variable type +// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. +// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). +// `constant`: Define a hidden constant. +// `datasource`: Quickly change the data source for an entire dashboard. +// `interval`: Interval variables represent time spans. +// `textbox`: Display a free text input field with an optional default value. +// `custom`: Define the variable options manually using a comma-separated list. +// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables +export type VariableType = "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | "system" | "snapshot"; + +export const defaultVariableType = (): VariableType => ("query"); + +// Custom formatter variable +export interface CustomFormatterVariable { + name: string; + type: VariableType; + multi: boolean; + includeAll: boolean; +} + +export const defaultCustomFormatterVariable = (): CustomFormatterVariable => ({ + name: "", + type: "query", + multi: false, + includeAll: false, +}); + +// FIXME: should we introduce this? --- Variable value option +export interface VariableValueOption { + label: string; + value: VariableValueSingle; + group?: string; +} + +export const defaultVariableValueOption = (): VariableValueOption => ({ + label: "", + value: defaultVariableValueSingle(), +}); + diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index ef556ab0d49..3c217f75312 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "@grafana/sql", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git", @@ -15,11 +15,11 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "11.6.0-pre", - "@grafana/e2e-selectors": "11.6.0-pre", + "@grafana/data": "12.0.0-pre", + "@grafana/e2e-selectors": "12.0.0-pre", "@grafana/plugin-ui": "0.10.1", - "@grafana/runtime": "11.6.0-pre", - "@grafana/ui": "11.6.0-pre", + "@grafana/runtime": "12.0.0-pre", + "@grafana/ui": "12.0.0-pre", "@react-awesome-query-builder/ui": "6.6.4", "immutable": "5.0.3", "lodash": "4.17.21", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e72cfe337bd..55785d7de92 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "11.6.0-pre", + "version": "12.0.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -66,10 +66,10 @@ "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", "@floating-ui/react": "0.27.5", - "@grafana/data": "11.6.0-pre", - "@grafana/e2e-selectors": "11.6.0-pre", + "@grafana/data": "12.0.0-pre", + "@grafana/e2e-selectors": "12.0.0-pre", "@grafana/faro-web-sdk": "^1.13.2", - "@grafana/schema": "11.6.0-pre", + "@grafana/schema": "12.0.0-pre", "@hello-pangea/dnd": "17.0.0", "@leeoniya/ufuzzy": "1.0.18", "@monaco-editor/react": "4.6.0", diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index 59cd9074a2d..fbb2fecb085 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -10,6 +10,7 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { IconName } from '../../types'; import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton'; import { Icon } from '../Icon/Icon'; +import { PopoverContent } from '../Tooltip'; import { Tooltip } from '../Tooltip/Tooltip'; export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple' | 'darkgrey'; @@ -18,7 +19,7 @@ export interface BadgeProps extends HTMLAttributes { text: React.ReactNode; color: BadgeColor; icon?: IconName; - tooltip?: string; + tooltip?: PopoverContent; } const BadgeComponent = React.memo(({ icon, color, text, tooltip, className, ...otherProps }) => { diff --git a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx index 34e0f95db20..caab6779ff1 100644 --- a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx +++ b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx @@ -126,5 +126,7 @@ const collapsableSectionStyles = (theme: GrafanaTheme2) => ({ }), label: css({ display: 'flex', + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.maxContrast, }), }); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx index 8b81be33bf0..9f13e67918b 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx @@ -12,6 +12,24 @@ describe('ColorPickerInput', () => { expect(screen.getByTestId('color-popover')).toBeInTheDocument(); }); + it('should hide color popover on blur', async () => { + render(); + expect(screen.queryByTestId('color-popover')).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole('textbox')); + expect(screen.getByTestId('color-popover')).toBeInTheDocument(); + await userEvent.click(document.body); + expect(screen.queryByTestId('color-popover')).not.toBeInTheDocument(); + }); + + it('should not hide color popover on blur if clicked inside the color picker', async () => { + render(); + expect(screen.queryByTestId('color-popover')).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole('textbox')); + expect(screen.getByTestId('color-popover')).toBeInTheDocument(); + await userEvent.click(screen.getAllByRole('slider')[0]); + expect(screen.queryByTestId('color-popover')).toBeInTheDocument(); + }); + it('should pass correct color to onChange callback', async () => { const mockOnChange = jest.fn(); render(); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx index add799af11c..381ad0403b2 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useState, forwardRef } from 'react'; +import { useState, forwardRef, FocusEvent } from 'react'; import { RgbaStringColorPicker } from 'react-colorful'; import { useThrottleFn } from 'react-use'; @@ -48,6 +48,14 @@ export const ColorPickerInput = forwardRef) => { + // Unless the user clicked inside the color picker, close it on blur + const isClickInPopover = document.querySelector('[data-testid="color-popover"]')?.contains(evt.relatedTarget); + if (!isClickInPopover) { + setIsOpen(false); + } + }; + return ( setIsOpen(false)}>
@@ -66,7 +74,7 @@ export const ColorPickerInput = forwardRef setIsOpen(true)} - onBlur={() => setIsOpen(false)} + onBlur={(e) => handleBlur(e)} ref={ref} isClearable /> diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index f5803afdf2e..f2d5ca7de20 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -1,4 +1,4 @@ -import { autoUpdate, flip, size, useFloating } from '@floating-ui/react'; +import { autoUpdate, autoPlacement, size, useFloating } from '@floating-ui/react'; import { useMemo, useRef, useState } from 'react'; import { measureText } from '../../utils'; @@ -31,10 +31,11 @@ export const useComboboxFloat = (items: Array>, // the order of middleware is important! const middleware = [ - flip({ - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: true, + autoPlacement({ + // see https://floating-ui.com/docs/autoplacement + allowedPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end'], boundary: document.body, + crossAxis: true, }), size({ apply({ availableWidth, availableHeight }) { diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.test.ts b/packages/grafana-ui/src/components/Combobox/useOptions.test.ts new file mode 100644 index 00000000000..a80d6f01168 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useOptions.test.ts @@ -0,0 +1,156 @@ +import { renderHook, act, waitFor } from '@testing-library/react'; + +import { sortByGroup, useOptions } from './useOptions'; + +describe('useOptions', () => { + it('should handle a large number of synchronous options without throwing an error', () => { + const largeOptions = Array.from({ length: 1_000_000 }, (_, i) => ({ + label: `Option ${i + 1}`, + value: `${i + 1}`, + })); + const { result } = renderHook(() => useOptions(largeOptions, false)); + + act(() => { + result.current.updateOptions('Option 999999'); + }); + + expect(result.current.options).toEqual([{ label: 'Option 999999', value: '999999' }]); + }); + + it('should return filtered options for synchronous options', () => { + const options = [ + { label: 'Option 1', value: '1' }, + { label: 'Option 2', value: '2' }, + ]; + const { result } = renderHook(() => useOptions(options, false)); + + act(() => { + result.current.updateOptions('Option 1'); + }); + + expect(result.current.options).toEqual([{ label: 'Option 1', value: '1' }]); + }); + + it('should handle asynchronous options', async () => { + const asyncOptions = jest.fn().mockResolvedValue([ + { label: 'Async Option 1', value: '1' }, + { label: 'Async Option 2', value: '2' }, + ]); + const { result } = renderHook(() => useOptions(asyncOptions, false)); + + act(() => { + result.current.updateOptions('Async'); + }); + + expect(result.current.asyncLoading).toBe(true); + + await waitFor(() => expect(result.current.asyncLoading).toBe(false)); + + expect(result.current.options).toEqual([ + { label: 'Async Option 1', value: '1' }, + { label: 'Async Option 2', value: '2' }, + ]); + }); + + it('should add a custom value if enabled', () => { + const options = [ + { label: 'Apple', value: 'apple' }, + { label: 'Carrot', value: 'carrot' }, + ]; + const { result } = renderHook(() => useOptions(options, true)); + + act(() => { + result.current.updateOptions('car'); + }); + + expect(result.current.options).toEqual([ + { label: 'car', value: 'car', description: 'Use custom value' }, + { label: 'Carrot', value: 'carrot' }, + ]); + }); + + it('should not add a custom value if it already exists', () => { + const options = [ + { label: 'Apple', value: 'apple' }, + { label: 'Carrot', value: 'carrot' }, + ]; + const { result } = renderHook(() => useOptions(options, true)); + + act(() => { + result.current.updateOptions('carrot'); + }); + + expect(result.current.options).toEqual([{ label: 'Carrot', value: 'carrot' }]); + }); + + it('should handle errors in asynchronous options', async () => { + jest.spyOn(console, 'error').mockImplementation(); + + const asyncOptions = jest.fn().mockRejectedValue(new Error('Async error')); + const { result } = renderHook(() => useOptions(asyncOptions, false)); + + act(() => { + result.current.updateOptions('Async'); + }); + + expect(result.current.asyncLoading).toBe(true); + + await waitFor(() => expect(result.current.asyncLoading).toBe(false)); + + expect(result.current.asyncLoading).toBe(false); + expect(result.current.asyncError).toBe(true); + }); +}); + +describe('sortByGroup', () => { + it('should return original array when no groups exist', () => { + const options = [ + { label: 'Apple', value: 'apple' }, + { label: 'Banana', value: 'banana' }, + { label: 'Carrot', value: 'carrot' }, + ]; + + const { options: sortedOptions, groupStartIndices } = sortByGroup(options); + + expect(sortedOptions).toBe(options); // Check reference equality + expect(groupStartIndices.size).toBe(0); + }); + + it('should return original array when only one group exists', () => { + const options = [ + { label: 'Apple', value: 'apple', group: 'fruits' }, + { label: 'Banana', value: 'banana', group: 'fruits' }, + { label: 'Tomato', value: 'tomato', group: 'fruits' }, + ]; + + const { options: sortedOptions, groupStartIndices } = sortByGroup(options); + + expect(sortedOptions).toEqual(options); + expect(groupStartIndices.size).toBe(1); + expect(groupStartIndices.get('fruits')).toBe(0); + }); + + it('should group options and track group start indices', () => { + const options = [ + { label: 'Apple', value: 'apple', group: 'fruits' }, + { label: 'Carrot', value: 'carrot', group: 'vegetables' }, + { label: 'Banana', value: 'banana', group: 'fruits' }, + { label: 'Celery', value: 'celery', group: 'vegetables' }, + { label: 'Other', value: 'other' }, // Ungrouped + ]; + + const { options: sortedOptions, groupStartIndices } = sortByGroup(options); + + expect(sortedOptions).toEqual([ + { label: 'Apple', value: 'apple', group: 'fruits' }, + { label: 'Banana', value: 'banana', group: 'fruits' }, + { label: 'Carrot', value: 'carrot', group: 'vegetables' }, + { label: 'Celery', value: 'celery', group: 'vegetables' }, + { label: 'Other', value: 'other' }, + ]); + + expect(groupStartIndices.size).toBe(2); + expect(groupStartIndices.get('fruits')).toBe(0); + expect(groupStartIndices.get('vegetables')).toBe(2); + }); +}); diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts index 7801fe5b007..bedd6dc1b8b 100644 --- a/packages/grafana-ui/src/components/Combobox/useOptions.ts +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -1,3 +1,6 @@ +/* Spreading unbound arrays can be very slow or even crash the browser if used for arguments */ +/* eslint no-restricted-syntax: ["error", "SpreadElement"] */ + import { debounce } from 'lodash'; import { useState, useCallback, useMemo } from 'react'; @@ -14,7 +17,7 @@ type AsyncOptions = const asyncNoop = () => Promise.resolve([]); /** - * Abstracts away sync/async options for MultiCombobox (and later Combobox). + * Abstracts away sync/async options for combobox components. * It also filters options based on the user's input. * * Returns: @@ -66,14 +69,11 @@ export function useOptions(rawOptions: AsyncOptions opt.value === userTypedSearch); if (!customValueExists) { - currentOptions = [ - { - label: userTypedSearch, - value: userTypedSearch as T, - description: t('combobox.custom-value.description', 'Use custom value'), - }, - ...currentOptions, - ]; + currentOptions.unshift({ + label: userTypedSearch, + value: userTypedSearch as T, + description: t('combobox.custom-value.description', 'Use custom value'), + }); } } return currentOptions; @@ -115,41 +115,61 @@ export function useOptions(rawOptions: AsyncOptions(options: Array>) { +/** + * Sorts options by group and returns the sorted options and the starting index of each group + */ +export function sortByGroup(options: Array>) { + // Group options by their group const groupedOptions = new Map>>(); + const groupStartIndices = new Map(); + for (const option of options) { - const groupExists = groupedOptions.has(option.group); - if (groupExists) { - groupedOptions.get(option.group)?.push(option); + const group = option.group; + const existing = groupedOptions.get(group); + if (existing) { + existing.push(option); } else { - groupedOptions.set(option.group, [option]); + groupedOptions.set(group, [option]); } } - // Create a map to track the starting index of each group - const groupStartIndices = new Map(); + // If we only have one group (either the undefined group, or a single group), return the original array + if (groupedOptions.size <= 1) { + if (options[0]?.group) { + groupStartIndices.set(options[0]?.group, 0); + } + + return { + options, + groupStartIndices, + }; + } + + // 'Preallocate' result array with same size as input - very minor optimization + const result: Array> = new Array(options.length); + let currentIndex = 0; - // Reorganize options to have groups first, then undefined group - const reorganizeOptions = []; + // Fill result array with grouped options for (const [group, groupOptions] of groupedOptions) { - if (!group) { - continue; + if (group) { + groupStartIndices.set(group, currentIndex); + for (const option of groupOptions) { + result[currentIndex++] = option; + } } - - groupStartIndices.set(group, currentIndex); - reorganizeOptions.push(...groupOptions); - currentIndex += groupOptions.length; } - const undefinedGroupOptions = groupedOptions.get(undefined); - if (undefinedGroupOptions) { - groupStartIndices.set('undefined', currentIndex); - reorganizeOptions.push(...undefinedGroupOptions); + // Add ungrouped options at the end + const ungrouped = groupedOptions.get(undefined); + if (ungrouped) { + for (const option of ungrouped) { + result[currentIndex++] = option; + } } return { - options: reorganizeOptions, + options: result, groupStartIndices, }; } diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx index d07788bf97e..7ef005c42c6 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx @@ -1,8 +1,7 @@ import { css, cx } from '@emotion/css'; -import { useDialog } from '@react-aria/dialog'; +import { useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { FocusScope } from '@react-aria/focus'; -import { useOverlay } from '@react-aria/overlays'; -import { createRef, FormEvent, MouseEvent, useState } from 'react'; +import { FormEvent, MouseEvent, useState } from 'react'; import { dateTime, getDefaultTimeRange, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -79,22 +78,21 @@ export const TimeRangeInput = ({ onChange({ from, to, raw: { from, to } }); }; - const overlayRef = createRef(); - const buttonRef = createRef(); + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + placement: 'bottom-start', + strategy: 'fixed', + }); - const { dialogProps } = useDialog({}, overlayRef); - - const { overlayProps } = useOverlay( - { - onClose, - isDismissable: true, - isOpen, - shouldCloseOnInteractOutside: (element) => { - return !buttonRef.current?.contains(element); - }, + const dismiss = useDismiss(context, { + bubbles: { + outsidePress: false, }, - overlayRef - ); + }); + + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); + return (
{isOpen && ( -
+
{ marginLeft: 0, position: 'absolute', top: '116%', - zIndex: theme.zIndex.dropdown, + zIndex: theme.zIndex.modal, }), pickerInput: cx( inputStyles.input, diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 310ae7fe7b4..7b534d88b09 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -143,6 +143,7 @@ export function PanelChrome({ onFocus, onMouseMove, onMouseEnter, + onDragStart, showMenuAlways = false, }: PanelChromeProps) { const theme = useTheme2(); @@ -201,11 +202,13 @@ export function PanelChrome({ const onPointerUp = (evt: React.PointerEvent) => { evt.stopPropagation(); - const distance = Math.sqrt( - Math.pow(pointerDownPos.current.screenX - evt.screenX, 2) + - Math.pow(pointerDownPos.current.screenY - evt.screenY, 2) + const distance = Math.hypot( + pointerDownPos.current.screenX - evt.screenX, + pointerDownPos.current.screenY - evt.screenY ); + pointerDownPos.current = { screenX: 0, screenY: 0 }; + // If we are dragging some distance or clicking on elements that should cancel dragging (panel menu, etc) if ( distance > 10 || @@ -219,7 +222,10 @@ export function PanelChrome({ const onPointerDown = (evt: React.PointerEvent) => { evt.stopPropagation(); + pointerDownPos.current = { screenX: evt.screenX, screenY: evt.screenY }; + + onDragStart?.(evt); }; const headerContent = ( diff --git a/packages/grafana-ui/src/components/ScrollContainer/ScrollIndicators.tsx b/packages/grafana-ui/src/components/ScrollContainer/ScrollIndicators.tsx index 8d7819e7efe..2263b607db1 100644 --- a/packages/grafana-ui/src/components/ScrollContainer/ScrollIndicators.tsx +++ b/packages/grafana-ui/src/components/ScrollContainer/ScrollIndicators.tsx @@ -54,6 +54,9 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => { }; const getStyles = (theme: GrafanaTheme2) => { + // we specifically don't want a theme color here + // this gradient is more like a shadow + const scrollGradientColor = `rgba(0, 0, 0, ${theme.isDark ? 0.25 : 0.08})`; return { scrollContent: css({ display: 'flex', @@ -62,7 +65,7 @@ const getStyles = (theme: GrafanaTheme2) => { position: 'relative', }), scrollIndicator: css({ - height: theme.spacing(6), + height: `max(5%, ${theme.spacing(3)})`, left: 0, opacity: 0, pointerEvents: 'none', @@ -74,11 +77,11 @@ const getStyles = (theme: GrafanaTheme2) => { zIndex: 1, }), scrollTopIndicator: css({ - background: `linear-gradient(0deg, transparent, ${theme.colors.background.canvas})`, + background: `linear-gradient(0deg, transparent, ${scrollGradientColor})`, top: 0, }), scrollBottomIndicator: css({ - background: `linear-gradient(180deg, transparent, ${theme.colors.background.canvas})`, + background: `linear-gradient(180deg, transparent, ${scrollGradientColor})`, bottom: 0, }), scrollIndicatorVisible: css({ diff --git a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts index 144dbfa3b79..aa243c17c68 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts @@ -19,6 +19,13 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) { }, }, + '.dragging-active': { + '*': { + cursor: 'move', + userSelect: 'none', + }, + }, + [theme.breakpoints.down('md')]: { '.react-grid-layout': { height: 'unset !important', @@ -83,6 +90,14 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) { }, }, + '.dashboard-canvas-add-button': { + opacity: 0, + + '&:hover': { + opacity: 1, + }, + }, + '.dashboard-visible-hidden-element': { opacity: 0.6, diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index e6ca98240ed..f893303d595 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.23.7 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.272.0 + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -22,10 +22,10 @@ require ( require ( cel.dev/expr v0.19.1 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -39,7 +39,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v1.7.2 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -49,13 +49,13 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.23.2 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -77,8 +77,8 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -97,7 +97,7 @@ require ( github.com/onsi/ginkgo/v2 v2.22.0 // indirect github.com/onsi/gomega v1.36.1 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.0 // indirect @@ -125,7 +125,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -140,16 +140,16 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 0198e0efd67..49b2f24fbea 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -2,16 +2,17 @@ cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -60,8 +61,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -92,13 +93,13 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -112,8 +113,8 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -135,8 +136,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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -182,10 +183,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -247,8 +248,8 @@ github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7y github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -362,8 +363,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= @@ -446,8 +447,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -484,18 +485,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/api/admin_encryption.go b/pkg/api/admin_encryption.go index e7bb65070f2..74a04357042 100644 --- a/pkg/api/admin_encryption.go +++ b/pkg/api/admin_encryption.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - skv "github.com/grafana/grafana/pkg/services/secrets/kvstore" ) func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *contextmodel.ReqContext) response.Response { @@ -50,51 +49,3 @@ func (hs *HTTPServer) AdminRollbackSecrets(c *contextmodel.ReqContext) response. return response.Respond(http.StatusOK, "Secrets rolled back successfully") } - -// To migrate to the plugin, it must be installed and configured -// so as not to lose access to migrated secrets -func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *contextmodel.ReqContext) response.Response { - if skv.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) != nil { - hs.log.Warn("Received secrets plugin migration request while plugin is not available") - return response.Respond(http.StatusBadRequest, "Secrets plugin is not available") - } - err := hs.secretsPluginMigrator.TriggerPluginMigration(c.Req.Context(), true) - if err != nil { - hs.log.Error("Failed to trigger secret migration to plugin", "error", err.Error()) - return response.Respond(http.StatusInternalServerError, "Secret migration to plugin failed") - } - return response.Respond(http.StatusOK, "Secret migration to plugin triggered successfully") -} - -// To migrate from the plugin, it must be installed only -// as it is possible the user disabled it and then wants to migrate -func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *contextmodel.ReqContext) response.Response { - if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { - hs.log.Warn("Received secrets plugin migration request while plugin is not installed") - return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") - } - err := hs.secretsPluginMigrator.TriggerPluginMigration(c.Req.Context(), false) - if err != nil { - hs.log.Error("Failed to trigger secret migration from plugin", "error", err.Error()) - return response.Respond(http.StatusInternalServerError, "Secret migration from plugin failed") - } - return response.Respond(http.StatusOK, "Secret migration from plugin triggered successfully") -} - -func (hs *HTTPServer) AdminDeleteAllSecretsManagerPluginSecrets(c *contextmodel.ReqContext) response.Response { - if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { - hs.log.Warn("Received secrets plugin deletion request while plugin is not installed") - return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") - } - items, err := hs.secretsStore.GetAll(c.Req.Context()) - if err != nil { - return response.Respond(http.StatusInternalServerError, "an error occurred while retrieving secrets") - } - for _, item := range items { - err := hs.secretsStore.Del(c.Req.Context(), *item.OrgId, *item.Namespace, *item.Type) - if err != nil { - return response.Respond(http.StatusInternalServerError, fmt.Sprintf("error deleting key with org=%v namespace=%v type=%v. error=%v", *item.OrgId, *item.Namespace, *item.Type, err.Error())) - } - } - return response.Respond(http.StatusOK, fmt.Sprintf("All %d Secrets Manager plugin secrets deleted", len(items))) -} diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 4720e25e91f..d0e56d1c052 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -278,7 +277,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp } if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { - if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave { + if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave { return dashboardGuardianResponse(err) } } @@ -336,7 +335,7 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo } if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { - if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave { + if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave { return dashboardGuardianResponse(err) } } @@ -502,7 +501,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response. return resp } - if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave { + if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave { return dashboardGuardianResponse(err) } } @@ -518,25 +517,17 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response. return response.Success("Annotation deleted") } -func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, annotation *annotations.ItemDTO) (bool, error) { +func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, annotation *annotations.ItemDTO) (bool, error) { if annotation.GetType() == annotations.Dashboard { - return canEditDashboard(c, annotation.DashboardID) + return canEditDashboard(c, ac, annotation.DashboardID) } else { return true, nil } } -func canEditDashboard(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { - guard, err := guardian.New(c.Req.Context(), dashboardID, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return false, err - } - - if canEdit, err := guard.CanEdit(); err != nil || !canEdit { - return false, err - } - - return true, nil +func canEditDashboard(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, dashboardID int64) (bool, error) { + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10))) + return ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) } func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, user *user.SignedInUser) (*annotations.ItemDTO, response.Response) { @@ -642,23 +633,25 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature if annotation.DashboardID == 0 { return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil } else { - dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: annotation.DashboardID, OrgID: orgID}) - if err != nil { - return nil, err - } - scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)} - // Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder - if dashboard.FolderUID != "" { - scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) - inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc) + return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { + dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: annotation.DashboardID, OrgID: orgID}) if err != nil { return nil, err } - scopes = append(scopes, inheritedScopes...) - } else { - scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)) - } - return scopes, nil + scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)} + // Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder + if dashboard.FolderUID != "" { + scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc) + if err != nil { + return nil, err + } + scopes = append(scopes, inheritedScopes...) + } else { + scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)) + } + return scopes, nil + }) } }) } @@ -680,7 +673,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardI return canSave, err } - return canEditDashboard(c, dashboardId) + return canEditDashboard(c, hs.AccessControl, dashboardId) } else { // organization annotations evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) @@ -708,7 +701,7 @@ func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashb return false, err } - canSave, err = canEditDashboard(c, dashboardID) + canSave, err = canEditDashboard(c, hs.AccessControl, dashboardID) if err != nil || !canSave { return false, err } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 83e11eb91cf..6eaf4317be6 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" ) @@ -110,7 +109,10 @@ func TestAPI_Annotations(t *testing.T) { path: "/api/annotations/2", method: http.MethodPut, expectedCode: http.StatusOK, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + }, }, { desc: "should not be able to update dashboard annotation without correct permission", @@ -162,7 +164,10 @@ func TestAPI_Annotations(t *testing.T) { path: "/api/annotations/2", method: http.MethodPatch, expectedCode: http.StatusOK, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + }, }, { desc: "should not be able to patch dashboard annotation without correct permission", @@ -215,7 +220,10 @@ func TestAPI_Annotations(t *testing.T) { method: http.MethodPost, body: "{\"dashboardId\": 2,\"text\": \"test\"}", expectedCode: http.StatusOK, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + }, }, { desc: "should not be able to create dashboard annotation without correct permission", @@ -273,7 +281,10 @@ func TestAPI_Annotations(t *testing.T) { path: "/api/annotations/2", method: http.MethodDelete, expectedCode: http.StatusOK, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + }, }, { desc: "should not be able to delete dashboard annotation without correct permission", @@ -341,7 +352,10 @@ func TestAPI_Annotations(t *testing.T) { body: "{\"dashboardId\": 2, \"panelId\": 1}", method: http.MethodPost, expectedCode: http.StatusOK, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + }, }, { desc: "should not be able to mass delete dashboard annotations without correct permission", @@ -382,10 +396,6 @@ func TestAPI_Annotations(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - // Don't need access to dashboards if annotationPermissionUpdate is enabled - if len(tt.featureFlags) == 0 { - setUpRBACGuardian(t) - } server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = setting.NewCfg() repo := annotationstest.NewFakeAnnotationsRepo() @@ -426,8 +436,8 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { dashSvc := &dashboards.FakeDashboardService{} rootDash := &dashboards.Dashboard{ID: 1, OrgID: 1, UID: rootDashUID} folderDash := &dashboards.Dashboard{ID: 2, OrgID: 1, UID: folderDashUID, FolderUID: folderUID} - dashSvc.On("GetDashboard", context.Background(), &dashboards.GetDashboardQuery{ID: rootDash.ID, OrgID: 1}).Return(rootDash, nil) - dashSvc.On("GetDashboard", context.Background(), &dashboards.GetDashboardQuery{ID: folderDash.ID, OrgID: 1}).Return(folderDash, nil) + dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{ID: rootDash.ID, OrgID: 1}).Return(rootDash, nil) + dashSvc.On("GetDashboard", mock.Anything, &dashboards.GetDashboardQuery{ID: folderDash.ID, OrgID: 1}).Return(folderDash, nil) rootDashboardAnnotation := annotations.Item{ID: 1, DashboardID: rootDash.ID} folderDashboardAnnotation := annotations.Item{ID: 3, DashboardID: folderDash.ID} @@ -518,12 +528,3 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { }) } } - -func setUpRBACGuardian(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true, CanViewValue: true}) -} diff --git a/pkg/api/api.go b/pkg/api/api.go index bb4aa87dc5d..0026529cbd8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -117,6 +117,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/admin/orgs/edit/:id", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index) r.Get("/admin/stats", authorize(ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index) r.Get("/admin/provisioning", reqOrgAdmin, hs.Index) + r.Get("/admin/provisioning/*", reqOrgAdmin, hs.Index) if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index) @@ -462,22 +463,24 @@ func (hs *HTTPServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { - dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard)) + dashUIDScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(ac.Parameter(":uid")) + + dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead, dashUIDScope)), routing.Wrap(hs.GetDashboard)) if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.SoftDeleteDashboard)) + dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.SoftDeleteDashboard)) } else { - dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID)) + dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.DeleteDashboardByUID)) } dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) { - dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) - dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) - dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) + dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersions)) + dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDashboardVersion)) + dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersion)) if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - dashUidRoute.Patch("/trash", reqOrgAdmin, routing.Wrap(hs.RestoreDeletedDashboard)) - dashUidRoute.Delete("/trash", reqOrgAdmin, routing.Wrap(hs.HardDeleteDashboardByUID)) + dashUidRoute.Patch("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDeletedDashboard)) + dashUidRoute.Delete("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.HardDeleteDashboardByUID)) } dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { @@ -497,9 +500,10 @@ func (hs *HTTPServer) registerRoutes() { // Deprecated: use /uid/:uid API instead. dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { - dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) - dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) - dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) + dashIDScope := dashboards.ScopeDashboardsProvider.GetResourceScope(ac.Parameter(":dashboardId")) + dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersions)) + dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersion)) + dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.RestoreDashboardVersion)) dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { dashboardPermissionRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) @@ -573,9 +577,6 @@ func (hs *HTTPServer) registerRoutes() { adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys)) adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets)) adminRoute.Post("/encryption/rollback-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminRollbackSecrets)) - adminRoute.Post("/encryption/migrate-secrets/to-plugin", reqGrafanaAdmin, routing.Wrap(hs.AdminMigrateSecretsToPlugin)) - adminRoute.Post("/encryption/migrate-secrets/from-plugin", reqGrafanaAdmin, routing.Wrap(hs.AdminMigrateSecretsFromPlugin)) - adminRoute.Post("/encryption/delete-secretsmanagerplugin-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminDeleteAllSecretsManagerPluginSecrets)) adminRoute.Post("/provisioning/dashboards/reload", authorize(ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards)) adminRoute.Post("/provisioning/plugins/reload", authorize(ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins)) diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index b29d5000fad..19cf5323e56 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -44,9 +44,13 @@ func ToFolderErrorResponse(err error) response.Response { return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) } - // folder errors are wrapped in an error util, so this is the only way of comparing errors - if err.Error() == folder.ErrMaximumDepthReached.Error() { - return response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"}) + if errors.Is(err, folder.ErrMaximumDepthReached) { + return response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": folder.ErrMaximumDepthReached.Error()}) + } + + var statusErr *k8sErrors.StatusError + if errors.As(err, &statusErr) { + return response.Error(int(statusErr.ErrStatus.Code), statusErr.ErrStatus.Message, err) } return response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", err) diff --git a/pkg/api/apierrors/folder_test.go b/pkg/api/apierrors/folder_test.go index e3d70bded31..09cd5aba16b 100644 --- a/pkg/api/apierrors/folder_test.go +++ b/pkg/api/apierrors/folder_test.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/require" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestToFolderErrorResponse(t *testing.T) { @@ -66,13 +68,28 @@ func TestToFolderErrorResponse(t *testing.T) { { name: "folder max depth reached", input: folder.ErrMaximumDepthReached, - want: response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"}), + want: response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": folder.ErrMaximumDepthReached.Error()}), }, { name: "fallback error", input: errors.New("some error"), want: response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", errors.New("some error")), }, + { + name: "kubernetes status error", + input: &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: 412, + Message: "the folder has been changed by someone else", + }, + }, + want: response.Error(412, "the folder has been changed by someone else", &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: 412, + Message: "the folder has been changed by someone else", + }, + }), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index c1c61753676..4406b5d8891 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "reflect" "strconv" "strings" @@ -27,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" pref "github.com/grafana/grafana/pkg/services/preference" publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -141,18 +141,19 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response dash.Data.Set("id", dash.ID) } } - guardian, err := guardian.NewByDashboard(ctx, dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canView, err := guardian.CanView(); err != nil || !canView { - return dashboardGuardianResponse(err) + dashScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID) + writeEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashScope) + canSave, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, writeEvaluator) + canEdit := canSave + //nolint:staticcheck // ViewersCanEdit is deprecated but still used for backward compatibility + if hs.Cfg.ViewersCanEdit { + canEdit = true } - canEdit, _ := guardian.CanEdit() - canSave, _ := guardian.CanSave() - canAdmin, _ := guardian.CanAdmin() - canDelete, _ := guardian.CanDelete() + deleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashScope) + canDelete, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, deleteEvaluator) + adminEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashScope) + canAdmin, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, adminEvaluator) isStarred, err := hs.isDashboardStarredByUser(c, dash.ID) if err != nil { @@ -369,15 +370,6 @@ func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) respon return response.Error(http.StatusNotFound, "Dashboard not found", err) } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canRestore, err := guardian.CanSave(); err != nil || !canRestore { - return dashboardGuardianResponse(err) - } - err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID) if err != nil { var dashboardErr dashboards.DashboardErr @@ -417,16 +409,7 @@ func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.R return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canDelete, err := guardian.CanDelete(); err != nil || !canDelete { - return dashboardGuardianResponse(err) - } - - err = hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid) + err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -498,21 +481,12 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo } } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canDelete, err := guardian.CanDelete(); err != nil || !canDelete { - return dashboardGuardianResponse(err) - } - if dash.IsFolder { return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil) } // disconnect all library elements for this dashboard - err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) + err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) if err != nil { hs.log.Error( "Failed to disconnect library elements", @@ -840,14 +814,6 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - query := dashver.ListDashboardVersionsQuery{ OrgID: c.SignedInUser.GetOrgID(), DashboardID: dash.ID, @@ -959,15 +925,6 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Err(err) @@ -1027,22 +984,15 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) respons if err := web.Bind(c.Req, &apiOptions); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - guardianBase, err := guardian.New(c.Req.Context(), apiOptions.Base.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canSave, err := guardianBase.CanSave(); err != nil || !canSave { + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.Base.DashboardId, 10))) + if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { return dashboardGuardianResponse(err) } if apiOptions.Base.DashboardId != apiOptions.New.DashboardId { - guardianNew, err := guardian.New(c.Req.Context(), apiOptions.New.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardianNew.CanSave(); err != nil || !canSave { + evaluator = accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.New.DashboardId, 10))) + if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { return dashboardGuardianResponse(err) } } @@ -1159,21 +1109,19 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, DashboardUID: dash.UID, Version: apiCmd.Version, OrgID: c.SignedInUser.GetOrgID()} version, err := hs.dashboardVersionService.Get(c.Req.Context(), &versionQuery) if err != nil { return response.Error(http.StatusNotFound, "Dashboard version not found", nil) } + // do not allow restores if the json data is identical + // this is needed for the k8s flow, as the generation id will be used on the + // version table, and the generation id only increments when the actual spec is changed + if compareDashboardData(version.Data.MustMap(), dash.Data.MustMap()) { + return response.Error(http.StatusBadRequest, "Current dashboard is identical to the specified version", nil) + } + var userID int64 if id, err := identity.UserIdentifier(c.SignedInUser.GetID()); err == nil { userID = id @@ -1195,6 +1143,18 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon return hs.postDashboard(c, saveCmd) } +func compareDashboardData(versionData, dashData map[string]any) bool { + // these can be different but the actual data is the same + delete(versionData, "version") + delete(dashData, "version") + delete(versionData, "id") + delete(dashData, "id") + delete(versionData, "uid") + delete(dashData, "uid") + + return reflect.DeepEqual(versionData, dashData) +} + // swagger:route GET /dashboards/tags dashboards getDashboardTags // // Get all dashboards tags of an organisation. diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index b583c550da7..4ebe1e8708f 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "time" "github.com/grafana/grafana/pkg/api/dtos" @@ -17,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" @@ -226,21 +226,15 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon dashboardID := queryResult.Dashboard.Get("id").MustInt64() if dashboardID != 0 { - g, err := guardian.New(c.Req.Context(), dashboardID, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - if !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Err(err) - } - } else { - canEdit, err := g.CanEdit() - // check for permissions only if the dashboard is found - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) - } + evaluator := ac.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10))) + canEdit, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) + // check for permissions only if the dashboard is found + if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) + } - if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil) - } + if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil) } } diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 24dcd3ef4ec..01d9e6ce4d0 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -15,13 +15,12 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -41,7 +40,7 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) { hs.DashboardService = svc hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) + hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(svc, nil)) }) } @@ -378,6 +377,7 @@ func buildHttpServer(d dashboardsnapshots.Service, snapshotEnabled bool) *HTTPSe Cfg: &setting.Cfg{ SnapshotEnabled: snapshotEnabled, }, + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, } return hs } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3ed18a073e8..c32c3d1835c 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "encoding/json" "fmt" @@ -22,8 +23,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -134,7 +137,10 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive { nil, &usagestats.UsageStatsMock{T: t}, nil, - features, acimpl.ProvideAccessControl(features), &dashboards.FakeDashboardService{}, annotationstest.NewFakeAnnotationsRepo(), nil) + features, acimpl.ProvideAccessControl(features), + &dashboards.FakeDashboardService{}, + annotationstest.NewFakeAnnotationsRepo(), + nil, nil) require.NoError(t, err) return gLive } @@ -327,12 +333,16 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.starService = startest.NewStarServiceFake() - hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{ - ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{}, - ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, + expectedDashVersions := []*dashver.DashboardVersionDTO{ + {Data: simplejson.NewFromAny(map[string]any{"title": "Dash"})}, + {Data: simplejson.NewFromAny(map[string]any{"title": "Dash updated"})}, } - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) + hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{ + ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{}, + ExpectedDashboardVersions: expectedDashVersions, + ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, + } }) } @@ -344,6 +354,17 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { return server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/versions"), userWithPermissions(1, permissions))) } + calculateDiff := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) { + cmd := &dtos.CalculateDiffOptions{ + Base: dtos.CalculateDiffTarget{DashboardId: 1, Version: 1}, + New: dtos.CalculateDiffTarget{DashboardId: 1, Version: 2}, + DiffType: "json", + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + return server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/calculate-diff", bytes.NewReader(jsonBytes)), userWithPermissions(1, permissions))) + } + t.Run("Should not be able to list dashboard versions without correct permission", func(t *testing.T) { server := setup() @@ -363,7 +384,6 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { server := setup() permissions := []accesscontrol.Permission{ - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"}, } @@ -378,6 +398,28 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { require.NoError(t, res.Body.Close()) }) + + t.Run("Should be able to diff dashboards with correct permissions", func(t *testing.T) { + server := setup() + + permissions := []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + } + + res, err := calculateDiff(server, permissions) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("Should not be able to diff dashboards without permissions", func(t *testing.T) { + server := setup() + + res, err := calculateDiff(server, []accesscontrol.Permission{}) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) } func TestDashboardAPIEndpoint(t *testing.T) { @@ -527,39 +569,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }), }, } - sqlmock := dbtest.NewFakeDB() - cmd := dtos.CalculateDiffOptions{ - Base: dtos.CalculateDiffTarget{ - DashboardId: 1, - Version: 1, - }, - New: dtos.CalculateDiffTarget{ - DashboardId: 2, - Version: 2, - }, - DiffType: "basic", - } - - t.Run("when user does not have permission", func(t *testing.T) { - role := org.RoleViewer - postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: false}) - - callPostDashboard(sc) - assert.Equal(t, http.StatusForbidden, sc.resp.Code) - }, sqlmock, fakeDashboardVersionService) - }) - - t.Run("when user does have permission", func(t *testing.T) { - role := org.RoleAdmin - postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - // This test shouldn't hit GetDashboardACLInfoList, so no setup needed - sc.dashboardVersionService = fakeDashboardVersionService - callPostDashboard(sc) - assert.Equal(t, http.StatusOK, sc.resp.Code) - }, sqlmock, fakeDashboardVersionService) - }) }) t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) { @@ -584,15 +593,12 @@ func TestDashboardAPIEndpoint(t *testing.T) { { DashboardID: 2, Version: 1, - Data: fakeDash.Data, + Data: simplejson.NewFromAny(map[string]any{ + "title": "Dash1", + }), }, } mockSQLStore := dbtest.NewFakeDB() - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - t.Cleanup(func() { - guardian.New = origNewGuardian - }) restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { @@ -603,6 +609,72 @@ func TestDashboardAPIEndpoint(t *testing.T) { }, mockSQLStore) }) + t.Run("Should not be able to restore to the same data", func(t *testing.T) { + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 + fakeDash.HasACL = false + + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) + + cmd := dtos.RestoreDashboardVersionCommand{ + Version: 1, + } + fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() + fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersionDTO{ + { + DashboardID: 2, + Version: 1, + Data: fakeDash.Data, + }, + } + mockSQLStore := dbtest.NewFakeDB() + + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", + "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { + sc.dashboardVersionService = fakeDashboardVersionService + + callRestoreDashboardVersion(sc) + assert.Equal(t, http.StatusBadRequest, sc.resp.Code) + }, mockSQLStore) + }) + + t.Run("Given dashboard in general folder being restored should restore to general folder", func(t *testing.T) { + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 + fakeDash.HasACL = false + + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { + cmd := args.Get(1).(*dashboards.SaveDashboardDTO) + cmd.Dashboard = &dashboards.Dashboard{ + ID: 2, UID: "uid", Title: "Dash", Slug: "dash", Version: 1, + } + }).Return(nil, nil) + + fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() + fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersionDTO{ + { + DashboardID: 2, + Version: 1, + Data: simplejson.NewFromAny(map[string]any{ + "title": "Dash1", + }), + }, + } + + cmd := dtos.RestoreDashboardVersionCommand{ + Version: 1, + } + mockSQLStore := dbtest.NewFakeDB() + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", + "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { + callRestoreDashboardVersion(sc) + assert.Equal(t, http.StatusOK, sc.resp.Code) + }, mockSQLStore) + }) + t.Run("Given dashboard in general folder being restored should restore to general folder", func(t *testing.T) { fakeDash := dashboards.NewDashboard("Child dash") fakeDash.ID = 2 @@ -622,7 +694,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { { DashboardID: 2, Version: 1, - Data: fakeDash.Data, + Data: simplejson.NewFromAny(map[string]any{ + "title": "Dash1", + }), }, } @@ -648,7 +722,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { require.NoError(t, err) qResult := &dashboards.Dashboard{ID: 1, Data: dataValue} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) @@ -678,7 +751,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), @@ -710,7 +783,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { Data: dataValue, } dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { hs := &HTTPServer{ @@ -718,7 +790,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), @@ -753,7 +825,7 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { Cfg: cfg, pluginStore: &pluginstore.FakePluginStore{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, Features: featuremgmt.WithFeatures(), DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, @@ -765,13 +837,8 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { } } - setUp := func() { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - } - loggedInUserScenarioWithRole(t, "When user exists and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -797,7 +864,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When user does not exist and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -823,7 +889,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When failing to get user and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -882,6 +947,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(), ) require.NoError(t, err) dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions) @@ -891,6 +957,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(), ) require.NoError(t, err) @@ -978,47 +1045,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s }) } -func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, - role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, -) { - t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - cfg := setting.NewCfg() - - dashSvc := dashboards.NewFakeDashboardService(t) - hs := HTTPServer{ - Cfg: cfg, - ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), - Live: newTestLive(t, db.InitTestDB(t)), - QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &libraryelementsfake.LibraryElementService{}, - SQLStore: sqlmock, - dashboardVersionService: fakeDashboardVersionService, - Features: featuremgmt.WithFeatures(), - DashboardService: dashSvc, - tracer: tracing.InitializeTracerForTest(), - } - - sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { - c.Req.Body = mockRequestBody(cmd) - c.Req.Header.Add("Content-Type", "application/json") - sc.context = c - sc.context.SignedInUser = &user.SignedInUser{ - OrgID: testOrgID, - UserID: testUserID, - } - sc.context.OrgRole = role - - return hs.CalculateDashboardDiff(c) - }) - - sc.m.Post(routePattern, sc.defaultHandler) - - fn(sc) - }) -} - func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB, diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 0eef6a7ce16..8906ab381a4 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -26,7 +26,6 @@ import ( ) var datasourcesLogger = log.New("datasources") -var secretsPluginError datasources.ErrDatasourceSecretsPluginUserFriendly // swagger:route GET /datasources datasources getDataSources // @@ -178,9 +177,6 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response. err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { - if errors.As(err, &secretsPluginError) { - return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) - } return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } @@ -257,9 +253,6 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { - if errors.As(err, &secretsPluginError) { - return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) - } return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } @@ -307,9 +300,6 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *contextmodel.ReqContext) respons cmd := &datasources.DeleteDataSourceCommand{Name: name, OrgID: c.SignedInUser.GetOrgID()} err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { - if errors.As(err, &secretsPluginError) { - return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) - } return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } @@ -405,10 +395,6 @@ func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Respons return response.Error(http.StatusConflict, err.Error(), err) } - if errors.As(err, &secretsPluginError) { - return response.Error(http.StatusInternalServerError, "Failed to add datasource: "+err.Error(), err) - } - return response.ErrOrFallback(http.StatusInternalServerError, "Failed to add datasource", err) } @@ -532,10 +518,6 @@ func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datas return response.Error(http.StatusConflict, "Datasource has already been updated by someone else. Please reload and try again", err) } - if errors.As(err, &secretsPluginError) { - return response.Error(http.StatusInternalServerError, "Failed to update datasource: "+err.Error(), err) - } - return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update datasource", err) } diff --git a/pkg/api/ds_query.go b/pkg/api/ds_query.go index 2ff08a727b7..eee746165b2 100644 --- a/pkg/api/ds_query.go +++ b/pkg/api/ds_query.go @@ -29,11 +29,6 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons return response.Error(http.StatusNotFound, "Data source not found", err) } - var secretsPlugin datasources.ErrDatasourceSecretsPluginUserFriendly - if errors.As(err, &secretsPlugin) { - return response.Error(http.StatusInternalServerError, fmt.Sprint("Secrets Plugin error: ", err.Error()), err) - } - return response.ErrOrFallback(http.StatusInternalServerError, "Query data error", err) } diff --git a/pkg/api/ds_query_test.go b/pkg/api/ds_query_test.go index 60d6d3a2a47..f0f20e7a7ad 100644 --- a/pkg/api/ds_query_test.go +++ b/pkg/api/ds_query_test.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "context" "encoding/json" "errors" @@ -25,7 +24,6 @@ import ( fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/query" @@ -40,11 +38,6 @@ type fakeDataSourceRequestValidator struct { err error } -type secretsErrorResponseBody struct { - Error string `json:"error"` - Message string `json:"message"` -} - func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error { return rv.err } @@ -93,64 +86,6 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) { }) } -func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) { - cfg := setting.NewCfg() - ds := &fakeDatasources.FakeDataSourceService{SimulatePluginFailure: true} - db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound} - pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(), - &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "grafana", - }, - }, - }, - }, - &fakeDatasources.FakeCacheService{}, - ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider(), - ) - qds := query.ProvideService( - cfg, - nil, - nil, - &fakeDataSourceRequestValidator{}, - &fakePluginClient{ - QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp := backend.Responses{ - "A": backend.DataResponse{ - Error: errors.New("query failed"), - }, - } - return &backend.QueryDataResponse{Responses: resp}, nil - }, - }, - pcp, - ) - httpServer := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.queryDataService = qds - hs.QuotaService = quotatest.New(false, nil) - hs.pluginContextProvider = pcp - }) - - t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) { - req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(reqValid)) - webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}}) - resp, err := httpServer.SendJSON(req) - require.NoError(t, err) - require.Equal(t, http.StatusInternalServerError, resp.StatusCode) - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(resp.Body) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - var resObj secretsErrorResponseBody - err = json.Unmarshal(buf.Bytes(), &resObj) - require.NoError(t, err) - require.Equal(t, "", resObj.Error) - require.Contains(t, resObj.Message, "Secrets Plugin error:") - }) -} - var reqValid = `{ "from": "", "to": "", diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 997c52eb8b0..19306e3d4aa 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -154,20 +154,21 @@ type FrontendSettingsSqlConnectionLimitsDTO struct { } type FrontendSettingsDTO struct { - DefaultDatasource string `json:"defaultDatasource"` - Datasources map[string]plugins.DataSourceDTO `json:"datasources"` - MinRefreshInterval string `json:"minRefreshInterval"` - Panels map[string]plugins.PanelDTO `json:"panels"` - Apps map[string]*plugins.AppDTO `json:"apps"` - AppUrl string `json:"appUrl"` - AppSubUrl string `json:"appSubUrl"` - AllowOrgCreate bool `json:"allowOrgCreate"` - AuthProxyEnabled bool `json:"authProxyEnabled"` - LdapEnabled bool `json:"ldapEnabled"` - JwtHeaderName string `json:"jwtHeaderName"` - JwtUrlLogin bool `json:"jwtUrlLogin"` - LiveEnabled bool `json:"liveEnabled"` - AutoAssignOrg bool `json:"autoAssignOrg"` + DefaultDatasource string `json:"defaultDatasource"` + Datasources map[string]plugins.DataSourceDTO `json:"datasources"` + MinRefreshInterval string `json:"minRefreshInterval"` + Panels map[string]plugins.PanelDTO `json:"panels"` + Apps map[string]*plugins.AppDTO `json:"apps"` + AppUrl string `json:"appUrl"` + AppSubUrl string `json:"appSubUrl"` + AllowOrgCreate bool `json:"allowOrgCreate"` + AuthProxyEnabled bool `json:"authProxyEnabled"` + LdapEnabled bool `json:"ldapEnabled"` + JwtHeaderName string `json:"jwtHeaderName"` + JwtUrlLogin bool `json:"jwtUrlLogin"` + LiveEnabled bool `json:"liveEnabled"` + LiveMessageSizeLimit int `json:"liveMessageSizeLimit"` + AutoAssignOrg bool `json:"autoAssignOrg"` VerifyEmailEnabled bool `json:"verifyEmailEnabled"` SigV4AuthEnabled bool `json:"sigV4AuthEnabled"` @@ -228,7 +229,6 @@ type FrontendSettingsDTO struct { RendererDefaultImageWidth int `json:"rendererDefaultImageWidth"` RendererDefaultImageHeight int `json:"rendererDefaultImageHeight"` RendererDefaultImageScale float64 `json:"rendererDefaultImageScale"` - SecretsManagerPluginEnabled bool `json:"secretsManagerPluginEnabled"` Http2Enabled bool `json:"http2Enabled"` GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"` PluginCatalogURL string `json:"pluginCatalogURL"` @@ -269,9 +269,8 @@ type FrontendSettingsDTO struct { PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"` - CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"` - CloudMigrationFeedbackURL string `json:"cloudMigrationFeedbackURL"` - CloudMigrationPollIntervalMs int `json:"cloudMigrationPollIntervalMs"` + CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"` + CloudMigrationPollIntervalMs int `json:"cloudMigrationPollIntervalMs"` DateFormats setting.DateFormats `json:"dateFormats,omitempty"` diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 5847529c941..03a9ab1cf63 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -18,8 +18,10 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -476,6 +478,8 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog sc.cfg, dashStore, folderStore, features, folderPermissions, ac, folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sc.db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(b, err) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 840ae125594..96e8b75a0ec 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -768,3 +768,12 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) { }) } } + +func setUpRBACGuardian(t *testing.T) { + origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true, CanViewValue: true}) +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 83a1eeb2838..070e977af90 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -23,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/util" @@ -174,7 +173,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro } hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) - secretsManagerPluginEnabled := kvstore.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) == nil trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for")) isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget featureToggles := hs.Features.GetEnabled(c.Req.Context()) @@ -196,6 +194,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, LiveEnabled: hs.Cfg.LiveMaxConnections != 0, + LiveMessageSizeLimit: hs.Cfg.LiveMessageSizeLimit, AutoAssignOrg: hs.Cfg.AutoAssignOrg, VerifyEmailEnabled: hs.Cfg.VerifyEmailEnabled, SigV4AuthEnabled: hs.Cfg.SigV4AuthEnabled, @@ -240,7 +239,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro PublicDashboardAccessToken: c.PublicDashboardAccessToken, PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled, CloudMigrationIsTarget: isCloudMigrationTarget, - CloudMigrationFeedbackURL: hs.Cfg.CloudMigration.FeedbackURL, CloudMigrationPollIntervalMs: int(hs.Cfg.CloudMigration.FrontendPollInterval.Milliseconds()), SharedWithMeFolderUID: folder.SharedWithMeFolderUID, RootFolderUID: accesscontrol.GeneralFolderUID, @@ -280,7 +278,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth, RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight, RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale, - SecretsManagerPluginEnabled: secretsManagerPluginEnabled, Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme, GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent, PluginCatalogURL: hs.Cfg.PluginCatalogURL, diff --git a/pkg/api/health.go b/pkg/api/health.go index db80f9cdaec..b564dd6ada2 100644 --- a/pkg/api/health.go +++ b/pkg/api/health.go @@ -15,7 +15,7 @@ func (hs *HTTPServer) databaseHealthy(ctx context.Context) bool { } err := hs.SQLStore.WithDbSession(ctx, func(session *db.Session) error { - _, err := session.Exec("SELECT 1") + _, err := session.Query("SELECT 1") return err }) healthy := err == nil diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 21f6f2dcd56..de1c4dc810c 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -169,10 +169,9 @@ type HTTPServer struct { Listener net.Listener EncryptionService encryption.Internal SecretsService secrets.Service - secretsPluginManager plugins.SecretsPluginManager secretsStore secretsKV.SecretsKVStore secretsMigrator secrets.Migrator - secretsPluginMigrator spm.SecretMigrationProvider + secretMigrationProvider spm.SecretMigrationProvider DataSourcesService datasources.DataSourceService cleanUpService *cleanup.CleanUpService tracer tracing.Tracer @@ -264,8 +263,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, starService star.Service, csrfService csrf.Service, managedPlugins managedplugins.Manager, playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, - secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service, - secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore, + secretsMigrator secrets.Migrator, secretsService secrets.Service, + secretMigrationProvider spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore, publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service, orgDeletionService org.DeletionService, teamService team.Service, accesscontrolService accesscontrol.Service, navTreeService navtree.Service, @@ -329,9 +328,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi SocialService: socialService, EncryptionService: encryptionService, SecretsService: secretsService, - secretsPluginManager: secretsPluginManager, secretsMigrator: secretsMigrator, - secretsPluginMigrator: secretsPluginMigrator, + secretMigrationProvider: secretMigrationProvider, secretsStore: secretsStore, DataSourcesService: dataSourcesService, searchUsersService: searchUsersService, diff --git a/pkg/apimachinery/errutil/errors.go b/pkg/apimachinery/errutil/errors.go index e8bc0e4b0f2..2bb0fb79c73 100644 --- a/pkg/apimachinery/errutil/errors.go +++ b/pkg/apimachinery/errutil/errors.go @@ -71,6 +71,17 @@ func UnprocessableEntity(msgID string, opts ...BaseOpt) Base { return NewBase(StatusUnprocessableEntity, msgID, opts...) } +// UnsupportedMediaType initializes a new [Base] error with reason StatusUnsupportedMediaType +// that is used to construct [Error]. The msgID is passed to the caller +// to serve as the base for user facing error messages. +// +// msgID should be structured as component.errorBrief, for example +// +// plugin.unsupportedMediaType +func UnsupportedMediaType(msgID string, opts ...BaseOpt) Base { + return NewBase(StatusUnsupportedMediaType, msgID, opts...) +} + // Conflict initializes a new [Base] error with reason StatusConflict // that is used to construct [Error]. The msgID is passed to the caller // to serve as the base for user facing error messages. diff --git a/pkg/apimachinery/errutil/status.go b/pkg/apimachinery/errutil/status.go index 6379dcdf447..edfede773ba 100644 --- a/pkg/apimachinery/errutil/status.go +++ b/pkg/apimachinery/errutil/status.go @@ -29,6 +29,10 @@ const ( // contained instructions. // HTTP status code 422. StatusUnprocessableEntity CoreStatus = "Unprocessable Entity" + // StatusUnsupportedMediaType means that the server does not support + // the request payload's media type. + // HTTP status code 415. + StatusUnsupportedMediaType CoreStatus = CoreStatus(metav1.StatusReasonUnsupportedMediaType) // StatusConflict means that the server cannot fulfill the request // there is a conflict in the current state of a resource // HTTP status code 409. @@ -107,6 +111,8 @@ func (s CoreStatus) HTTPStatus() int { return http.StatusGatewayTimeout case StatusUnprocessableEntity: return http.StatusUnprocessableEntity + case StatusUnsupportedMediaType: + return http.StatusUnsupportedMediaType case StatusConflict: return http.StatusConflict case StatusTooManyRequests: @@ -137,6 +143,8 @@ func (s CoreStatus) LogLevel() LogLevel { return LevelInfo case StatusTimeout: return LevelInfo + case StatusUnsupportedMediaType: + return LevelInfo case StatusUnprocessableEntity: return LevelInfo case StatusConflict: diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index e3222591c87..72e9166d58e 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -40,10 +40,10 @@ require ( golang.org/x/crypto v0.35.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 5b69199f357..482565aefa8 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -122,8 +122,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -147,10 +147,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 8699b3a3ad9..68b63762ce6 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -62,7 +62,7 @@ func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Req return WithRequester(ctx, r), r } -func WithProvisioningIdentitiy(ctx context.Context, namespace string) (context.Context, Requester, error) { +func WithProvisioningIdentity(ctx context.Context, namespace string) (context.Context, Requester, error) { ns, err := types.ParseNamespace(namespace) if err != nil { return nil, nil, err diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index c285c202bc7..67076f2e96a 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -86,6 +86,7 @@ type GrafanaMetaAccessor interface { GetMessage() string SetMessage(msg string) SetAnnotation(key string, val string) + GetAnnotation(key string) string SetBlob(v *BlobInfo) GetBlob() *BlobInfo @@ -143,9 +144,13 @@ type grafanaMetaAccessor struct { // required fields are missing. Fields that are not required return the default // value and are a no-op if set. func MetaAccessor(raw interface{}) (GrafanaMetaAccessor, error) { + if raw == nil { + return nil, fmt.Errorf("unable to read metadata from nil object") + } + obj, err := meta.Accessor(raw) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to read metadata from: %T, %s", raw, err) } // reflection to find title and other non object properties @@ -188,6 +193,14 @@ func (m *grafanaMetaAccessor) SetAnnotation(key string, val string) { m.obj.SetAnnotations(anno) } +func (m *grafanaMetaAccessor) GetAnnotation(key string) string { + anno := m.obj.GetAnnotations() + if anno != nil { + return anno[key] + } + return "" +} + func (m *grafanaMetaAccessor) get(key string) string { return m.obj.GetAnnotations()[key] } diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index d621bf3411f..2e5571c9f25 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -22,6 +22,26 @@ type JobList struct { Items []Job `json:"items,omitempty"` } +// HistoricJob is a history entry of Job. It is used to store Jobs that have been processed. +// +// The repository name and type are stored as labels. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type HistoricJob struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec JobSpec `json:"spec,omitempty"` + Status JobStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type HistoricJobList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []HistoricJob `json:"items,omitempty"` +} + // +enum type JobAction string @@ -104,16 +124,13 @@ type ExportJobOptions struct { Branch string `json:"branch,omitempty"` // Prefix in target file system - Prefix string `json:"prefix,omitempty"` + Path string `json:"path,omitempty"` // Include the identifier in the exported metadata Identifier bool `json:"identifier"` } type MigrateJobOptions struct { - // Target file prefix - Prefix string `json:"prefix,omitempty"` - // Preserve history (if possible) History bool `json:"history,omitempty"` diff --git a/pkg/apis/provisioning/v0alpha1/register.go b/pkg/apis/provisioning/v0alpha1/register.go index 9e5eec0b6cc..788c2bd176c 100644 --- a/pkg/apis/provisioning/v0alpha1/register.go +++ b/pkg/apis/provisioning/v0alpha1/register.go @@ -68,7 +68,35 @@ var JobResourceInfo = utils.NewResourceInfo(GROUP, VERSION, Reader: func(obj any) ([]interface{}, error) { m, ok := obj.(*Job) if !ok { - return nil, errors.New("expected Repository") + return nil, errors.New("expected Job") + } + + return []interface{}{ + m.Name, // may our may not be nice to read + m.CreationTimestamp.UTC().Format(time.RFC3339), + m.Spec.Action, + m.Status.State, + m.Status.Message, + }, nil + }, + }) + +var HistoricJobResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "historicjobs", "historicjob", "HistoricJob", + func() runtime.Object { return &HistoricJob{} }, // newObj + func() runtime.Object { return &HistoricJobList{} }, // newList + utils.TableColumns{ // Returned by `kubectl get`. Doesn't affect disk storage. + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + {Name: "Action", Type: "string"}, + {Name: "State", Type: "string"}, + {Name: "Message", Type: "string"}, + }, + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*HistoricJob) + if !ok { + return nil, errors.New("expected HistoricJob") } return []interface{}{ @@ -117,6 +145,8 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error { &ResourceStats{}, &Job{}, &JobList{}, + &HistoricJob{}, + &HistoricJobList{}, ) return nil } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 6ab9dfc8a11..2c22ee95346 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -132,6 +132,67 @@ func (in *HealthStatus) DeepCopy() *HealthStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HistoricJob) DeepCopyInto(out *HistoricJob) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoricJob. +func (in *HistoricJob) DeepCopy() *HistoricJob { + if in == nil { + return nil + } + out := new(HistoricJob) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HistoricJob) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HistoricJobList) DeepCopyInto(out *HistoricJobList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HistoricJob, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoricJobList. +func (in *HistoricJobList) DeepCopy() *HistoricJobList { + if in == nil { + return nil + } + out := new(HistoricJobList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HistoricJobList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HistoryItem) DeepCopyInto(out *HistoryItem) { *out = *in diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 571f26b2b27..0ae93afb00d 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -20,6 +20,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HealthStatus": schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoricJob": schema_pkg_apis_provisioning_v0alpha1_HistoricJob(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoricJobList": schema_pkg_apis_provisioning_v0alpha1_HistoricJobList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem": schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryList": schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Job": schema_pkg_apis_provisioning_v0alpha1_Job(ref), @@ -108,7 +110,7 @@ func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.Reference Format: "", }, }, - "prefix": { + "path": { SchemaProps: spec.SchemaProps{ Description: "Prefix in target file system", Type: []string{"string"}, @@ -336,6 +338,100 @@ func schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref common.ReferenceCall } } +func schema_pkg_apis_provisioning_v0alpha1_HistoricJob(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HistoricJob is a history entry of Job. It is used to store Jobs that have been processed.\n\nThe repository name and type are stored as labels.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobSpec", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_HistoricJobList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + 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/pkg/apis/provisioning/v0alpha1.HistoricJob"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoricJob", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + func schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -818,13 +914,6 @@ func schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref common.Referenc SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "prefix": { - SchemaProps: spec.SchemaProps{ - Description: "Target file prefix", - Type: []string{"string"}, - Format: "", - }, - }, "history": { SchemaProps: spec.SchemaProps{ Description: "Preserve history (if possible)", diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod index 4c61171ac6c..86b6e29cf97 100644 --- a/pkg/apis/secret/go.mod +++ b/pkg/apis/secret/go.mod @@ -5,7 +5,7 @@ go 1.23.7 require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.70.0 + google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.5 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.32.1 @@ -30,7 +30,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -45,7 +45,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -79,14 +79,14 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/pkg/apis/secret/go.sum b/pkg/apis/secret/go.sum index 0f270b903fb..540c90bd580 100644 --- a/pkg/apis/secret/go.sum +++ b/pkg/apis/secret/go.sum @@ -53,8 +53,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -99,8 +99,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -259,8 +259,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -292,18 +292,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go b/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go index c1b285caede..cffa2065ef0 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go +++ b/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go @@ -78,9 +78,9 @@ func (x *SecureValueDecryptRequest) GetNames() []string { type SecureValueDecryptResponseCollection struct { state protoimpl.MessageState `protogen:"open.v1"` // A map of secure value names and their decrypted values. - // The value may be empty if the requestor does not have permissions to read it, or if the value does not exist. + // The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist. // It will never return a 404 Not Found to avoid scanning of valid secure values. - DecryptedValues map[string]string `protobuf:"bytes,1,rep,name=decrypted_values,json=decryptedValues,proto3" json:"decrypted_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DecryptedValues map[string]*Result `protobuf:"bytes,1,rep,name=decrypted_values,json=decryptedValues,proto3" json:"decrypted_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -115,13 +115,95 @@ func (*SecureValueDecryptResponseCollection) Descriptor() ([]byte, []int) { return file_decrypt_proto_rawDescGZIP(), []int{1} } -func (x *SecureValueDecryptResponseCollection) GetDecryptedValues() map[string]string { +func (x *SecureValueDecryptResponseCollection) GetDecryptedValues() map[string]*Result { if x != nil { return x.DecryptedValues } return nil } +type Result struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *Result_Value + // *Result_ErrorMessage + Result isResult_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Result) Reset() { + *x = Result{} + mi := &file_decrypt_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Result) ProtoMessage() {} + +func (x *Result) ProtoReflect() protoreflect.Message { + mi := &file_decrypt_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Result.ProtoReflect.Descriptor instead. +func (*Result) Descriptor() ([]byte, []int) { + return file_decrypt_proto_rawDescGZIP(), []int{2} +} + +func (x *Result) GetResult() isResult_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *Result) GetValue() string { + if x != nil { + if x, ok := x.Result.(*Result_Value); ok { + return x.Value + } + } + return "" +} + +func (x *Result) GetErrorMessage() string { + if x != nil { + if x, ok := x.Result.(*Result_ErrorMessage); ok { + return x.ErrorMessage + } + } + return "" +} + +type isResult_Result interface { + isResult_Result() +} + +type Result_Value struct { + Value string `protobuf:"bytes,1,opt,name=value,proto3,oneof"` +} + +type Result_ErrorMessage struct { + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3,oneof"` +} + +func (*Result_Value) isResult_Result() {} + +func (*Result_ErrorMessage) isResult_Result() {} + var File_decrypt_proto protoreflect.FileDescriptor var file_decrypt_proto_rawDesc = string([]byte{ @@ -131,7 +213,7 @@ var file_decrypt_proto_rawDesc = string([]byte{ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xd9, 0x01, 0x0a, 0x24, 0x53, 0x65, + 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xea, 0x01, 0x0a, 0x24, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x6d, 0x0a, 0x10, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, @@ -141,23 +223,30 @@ var file_decrypt_proto_rawDesc = string([]byte{ 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x1a, 0x42, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, + 0x73, 0x1a, 0x53, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0x80, 0x01, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x72, 0x12, 0x68, - 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, - 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x64, 0x65, 0x63, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, + 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x80, 0x01, 0x0a, 0x14, 0x53, 0x65, + 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x72, 0x12, 0x68, 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x53, 0x65, 0x63, + 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, - 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, - 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, - 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x76, 0x30, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, - 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, + 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x3d, 0x5a, 0x3b, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, + 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, + 0x70, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x76, 0x30, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x31, 0x2f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, }) var ( @@ -172,21 +261,23 @@ func file_decrypt_proto_rawDescGZIP() []byte { return file_decrypt_proto_rawDescData } -var file_decrypt_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_decrypt_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_decrypt_proto_goTypes = []any{ (*SecureValueDecryptRequest)(nil), // 0: decrypt.SecureValueDecryptRequest (*SecureValueDecryptResponseCollection)(nil), // 1: decrypt.SecureValueDecryptResponseCollection - nil, // 2: decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry + (*Result)(nil), // 2: decrypt.Result + nil, // 3: decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry } var file_decrypt_proto_depIdxs = []int32{ - 2, // 0: decrypt.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry - 0, // 1: decrypt.SecureValueDecrypter.DecryptSecureValues:input_type -> decrypt.SecureValueDecryptRequest - 1, // 2: decrypt.SecureValueDecrypter.DecryptSecureValues:output_type -> decrypt.SecureValueDecryptResponseCollection - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 3, // 0: decrypt.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry + 2, // 1: decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry.value:type_name -> decrypt.Result + 0, // 2: decrypt.SecureValueDecrypter.DecryptSecureValues:input_type -> decrypt.SecureValueDecryptRequest + 1, // 3: decrypt.SecureValueDecrypter.DecryptSecureValues:output_type -> decrypt.SecureValueDecryptResponseCollection + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_decrypt_proto_init() } @@ -194,13 +285,17 @@ func file_decrypt_proto_init() { if File_decrypt_proto != nil { return } + file_decrypt_proto_msgTypes[2].OneofWrappers = []any{ + (*Result_Value)(nil), + (*Result_ErrorMessage)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_decrypt_proto_rawDesc), len(file_decrypt_proto_rawDesc)), NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto b/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto index 402ef684efd..bf328ff5217 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto +++ b/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto @@ -14,9 +14,16 @@ message SecureValueDecryptRequest { message SecureValueDecryptResponseCollection { // A map of secure value names and their decrypted values. - // The value may be empty if the requestor does not have permissions to read it, or if the value does not exist. + // The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist. // It will never return a 404 Not Found to avoid scanning of valid secure values. - map decrypted_values = 1; + map decrypted_values = 1; +} + +message Result { + oneof result { + string value = 1; + string error_message = 2; + } } service SecureValueDecrypter { diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 39a9e18756d..32fc5be8d16 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/apiserver -go 1.23.7 +go 1.24.1 require ( github.com/google/go-cmp v0.7.0 @@ -37,7 +37,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -51,7 +51,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -83,15 +83,15 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 1d54379d869..075fb395455 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -53,8 +53,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -103,8 +103,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -267,8 +267,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -300,18 +300,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apiserver/readonly/store.go b/pkg/apiserver/readonly/store.go new file mode 100644 index 00000000000..140b0f7c3c4 --- /dev/null +++ b/pkg/apiserver/readonly/store.go @@ -0,0 +1,160 @@ +package readonly + +import ( + "context" + "errors" + "net/http" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/registry/rest" +) + +var ( + // ErrOperationUnsupported is returned when a read operation is not supported by the underlying storage. + // If you wish to check for this, call IsOperationUnsupported instead. + // + // Some methods may even include the name of the object that caused the error. + ErrOperationUnsupported = &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "Operation not supported", + Code: http.StatusMethodNotAllowed, + Status: metav1.StatusFailure, + Reason: metav1.StatusReasonMethodNotAllowed, + Details: &metav1.StatusDetails{ + Causes: []metav1.StatusCause{ + { + Type: "OperationNotImplementedByInner", + Message: "Operation not supported by the wrapped storage", + }, + }, + }, + }, + } + + _ rest.Storage = readOnly{} + _ rest.Scoper = readOnly{} + _ rest.SingularNameProvider = readOnly{} + _ rest.StorageWithReadiness = readOnly{} + _ rest.Getter = readOnly{} + _ rest.Lister = readOnlyLister{} + _ rest.TableConvertor = readOnlyLister{} // due to rest.Lister + _ rest.Watcher = readOnly{} +) + +// ReadOnly wraps a storage interface and makes all methods read-only. +// +// One write operation is passed through: Destroy. This allows you to not have to have some complex clean up setup. +type readOnly struct { + inner rest.Storage +} + +// ReadOnlyLister is like ReadOnly, but also implements rest.Lister. +type readOnlyLister struct { + readOnly + inner rest.Lister +} + +func Wrap(store rest.Storage) rest.Storage { + ro := readOnly{inner: store} + if l, ok := store.(rest.Lister); ok { + return readOnlyLister{readOnly: ro, inner: l} + } + return ro +} + +func (ro readOnly) New() runtime.Object { + return ro.inner.New() +} + +func (ro readOnlyLister) NewList() runtime.Object { + return ro.inner.NewList() +} + +func (ro readOnly) Destroy() { + ro.inner.Destroy() +} + +func (ro readOnly) NamespaceScoped() bool { + if s, ok := ro.inner.(rest.Scoper); ok { + return s.NamespaceScoped() + } + return false +} + +func (ro readOnly) GetSingularName() string { + if s, ok := ro.inner.(rest.SingularNameProvider); ok { + return s.GetSingularName() + } + return "" +} + +// ReadinessCheck does not check for readiness if the inner storage does not implement it. +// In that case, the inner storage is always considered ready. +func (ro readOnly) ReadinessCheck() error { + if r, ok := ro.inner.(rest.StorageWithReadiness); ok { + return r.ReadinessCheck() + } + return nil +} + +func (ro readOnly) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + if r, ok := ro.inner.(rest.Getter); ok { + return r.Get(ctx, name, options) + } + + return nil, ro.unsupported(name) +} + +func (ro readOnlyLister) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { + return ro.inner.List(ctx, options) +} + +func (ro readOnlyLister) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return ro.inner.ConvertToTable(ctx, object, tableOptions) +} + +func (ro readOnly) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) { + if r, ok := ro.inner.(rest.Watcher); ok { + return r.Watch(ctx, options) + } + + return nil, ro.unsupported("") +} + +func (ro readOnly) unsupported(name string) error { + obj := ro.New() + gvk := obj.GetObjectKind().GroupVersionKind() + err := *ErrOperationUnsupported + err.ErrStatus.Details.Kind = gvk.Kind + err.ErrStatus.Details.Group = gvk.Group + err.ErrStatus.Details.Name = name + + return &err +} + +// IsOperationUnsupported checks the error for its reasoning. If it originated from the read-only wrapper, it will return true. +// When this returns true, it indicates the underlying storage does not support the operation despite us "announcing" it (by implementing an interface). +func IsOperationUnsupported(err error) bool { + var statusErr *apierrors.StatusError + if !errors.As(err, &statusErr) { + return false + } + + if statusErr.Status().Reason != metav1.StatusReasonMethodNotAllowed || + statusErr.Status().Details == nil || + len(statusErr.Status().Details.Causes) == 0 { + return false + } + + for _, detail := range statusErr.Status().Details.Causes { + if detail.Type == "OperationNotImplementedByInner" { + return true + } + } + + return false +} diff --git a/pkg/apiserver/registry/generic/storage.go b/pkg/apiserver/registry/generic/storage.go index 67b0efecb8d..184ad344db0 100644 --- a/pkg/apiserver/registry/generic/storage.go +++ b/pkg/apiserver/registry/generic/storage.go @@ -9,7 +9,9 @@ import ( ) func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, optsGetter generic.RESTOptionsGetter) (*registry.Store, error) { - strategy := NewStrategy(scheme, resourceInfo.GroupVersion()) + gv := resourceInfo.GroupVersion() + gv.Version = runtime.APIVersionInternal + strategy := NewStrategy(scheme, gv) store := ®istry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, @@ -30,6 +32,18 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o return store, nil } +func NewCompleteRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, optsGetter generic.RESTOptionsGetter) (*registry.Store, error) { + registryStore, err := NewRegistryStore(scheme, resourceInfo, optsGetter) + if err != nil { + return nil, err + } + strategy := NewCompleteStrategy(scheme, resourceInfo.GroupVersion()) + registryStore.CreateStrategy = strategy + registryStore.UpdateStrategy = strategy + registryStore.DeleteStrategy = strategy + return registryStore, nil +} + func NewRegistryStatusStore(scheme *runtime.Scheme, specStore *registry.Store) *StatusREST { gv := specStore.New().GetObjectKind().GroupVersionKind().GroupVersion() strategy := NewStatusStrategy(scheme, gv) diff --git a/pkg/apiserver/registry/generic/strategy.go b/pkg/apiserver/registry/generic/strategy.go index 6cb5dd918fd..e899b8aa90f 100644 --- a/pkg/apiserver/registry/generic/strategy.go +++ b/pkg/apiserver/registry/generic/strategy.go @@ -3,8 +3,6 @@ package generic import ( "context" - "github.com/grafana/grafana/pkg/apimachinery/utils" - apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -14,8 +12,12 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" + + "github.com/grafana/grafana/pkg/apimachinery/utils" ) +// genericStrategy allows for writing objects with spec fields. +// It ignores status fields, and does not allow for status updates. type genericStrategy struct { runtime.ObjectTyper names.NameGenerator @@ -81,20 +83,6 @@ func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime } else { _ = newMeta.SetStatus(status) } - - spec, err := newMeta.GetSpec() - if err != nil { - return - } - - oldSpec, err := oldMeta.GetSpec() - if err != nil { - return - } - - if !apiequality.Semantic.DeepEqual(spec, oldSpec) { - newMeta.SetGeneration(oldMeta.GetGeneration() + 1) - } } func (g *genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { @@ -125,6 +113,8 @@ func (g *genericStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime return nil } +// genericStatusStrategy allows for writing objects with status fields, however may not create them. +// It ignores spec and metadata fields, and does not allow for updates outside of the status field. type genericStatusStrategy struct { runtime.ObjectTyper names.NameGenerator @@ -190,6 +180,71 @@ func (g *genericStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old r return nil } +// genericCompleteStrategy allows for writing objects with spec and status fields. +// It does not ignore any fields, and allows for updates to both spec and status fields. +// This is the same as having separate stores for spec and status fields. +// +// This can be applied to both the root object and status subresource in a Kubernetes REST API. +type genericCompleteStrategy struct { + runtime.ObjectTyper + names.NameGenerator + + gv schema.GroupVersion +} + +// NewCompleteStrategy creates a new genericCompleteStrategy. +func NewCompleteStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion) *genericCompleteStrategy { + return &genericCompleteStrategy{typer, names.SimpleNameGenerator, gv} +} + +func (g *genericCompleteStrategy) NamespaceScoped() bool { + return true +} + +func (g *genericCompleteStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + fields := map[fieldpath.APIVersion]*fieldpath.Set{ + fieldpath.APIVersion(g.gv.String()): fieldpath.NewSet(), + } + + return fields +} + +func (g *genericCompleteStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + meta, err := utils.MetaAccessor(obj) + if err != nil { + return + } + meta.SetGeneration(1) +} + +func (g *genericCompleteStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {} + +func (g *genericCompleteStrategy) AllowCreateOnUpdate() bool { + return true +} + +func (g *genericCompleteStrategy) AllowUnconditionalUpdate() bool { + return true +} + +func (g *genericCompleteStrategy) Canonicalize(obj runtime.Object) {} + +func (g *genericCompleteStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + return nil +} + +func (g *genericCompleteStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return nil +} + +func (g *genericCompleteStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +func (g *genericCompleteStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + // GetAttrs returns labels and fields of an object. func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { accessor, err := meta.Accessor(obj) diff --git a/pkg/apiserver/registry/generic/strategy_test.go b/pkg/apiserver/registry/generic/strategy_test.go index 969332305a2..e7423e8540c 100644 --- a/pkg/apiserver/registry/generic/strategy_test.go +++ b/pkg/apiserver/registry/generic/strategy_test.go @@ -1,134 +1,465 @@ package generic_test import ( - "context" "testing" "github.com/grafana/grafana/pkg/apiserver/registry/generic" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/apis/example" ) -func TestPrepareForUpdate(t *testing.T) { - ctx := context.TODO() +func TestGenericStrategy(t *testing.T) { + t.Parallel() gv := schema.GroupVersion{Group: "test", Version: "v1"} - strategy := generic.NewStrategy(runtime.NewScheme(), gv) - oldObj := &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - Generation: 1, - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "bar"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Running"), - }, - } + t.Run("PrepareForUpdate", func(t *testing.T) { + t.Parallel() - testCases := []struct { - name string - newObj *example.Pod - oldObj *example.Pod - expectedGen int64 - expectedObj *example.Pod - }{ - { - name: "ignore status updates", - newObj: &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - Generation: 1, - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "bar"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Stopped"), - }, + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Generation: 1, }, - oldObj: oldObj.DeepCopy(), - expectedGen: 2, - expectedObj: &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - Generation: 1, - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "bar"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Running"), - }, + Spec: example.PodSpec{ + NodeSelector: map[string]string{"foo": "bar"}, }, - }, - { - name: "increment generation if spec changes", - newObj: &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - Generation: 1, - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "baz"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Running"), - }, + Status: example.PodStatus{ + Phase: example.PodPhase("Running"), }, - oldObj: oldObj.DeepCopy(), - expectedGen: 2, - expectedObj: &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - Generation: 2, - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "baz"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Running"), - }, - }, - }, - } + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - strategy.PrepareForUpdate(ctx, tc.newObj, tc.oldObj) - require.Equal(t, tc.expectedObj, tc.newObj) + t.Run("ignores status updates", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Status.Phase = example.PodPhase("Stopped") + expectedObj := obj.DeepCopy() + + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) }) - } + + t.Run("does not increment generation if annotations, labels, finalizers, or owner references change", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Annotations = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Labels = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Finalizers = []string{"foo"} + newObj.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: "foo"}} + + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + assert.Equal(t, map[string]string{"foo": "baz"}, newObj.ObjectMeta.Annotations) + assert.Equal(t, map[string]string{"foo": "baz"}, newObj.ObjectMeta.Labels) + assert.Equal(t, []string{"foo"}, newObj.ObjectMeta.Finalizers) + assert.Equal(t, []metav1.OwnerReference{{Name: "foo"}}, newObj.ObjectMeta.OwnerReferences) + assert.Equal(t, int64(1), newObj.Generation) + }) + + t.Run("does not increment generation if spec changes", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Spec.NodeSelector = map[string]string{"foo": "baz"} + expectedObj := newObj.DeepCopy() + + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + }) + + t.Run("PrepareForCreate", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: example.PodSpec{ + NodeSelector: map[string]string{"foo": "bar"}, + }, + Status: example.PodStatus{ + Phase: example.PodPhase("Running"), + }, + } + + t.Run("assigns generation=1", func(t *testing.T) { + t.Parallel() + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + obj := obj.DeepCopy() + + strategy.PrepareForCreate(t.Context(), obj) + require.Equal(t, int64(1), obj.Generation) + }) + + t.Run("clears status", func(t *testing.T) { + t.Parallel() + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + obj := obj.DeepCopy() + + strategy.PrepareForCreate(t.Context(), obj) + require.Equal(t, example.PodStatus{}, obj.Status) + }) + + t.Run("leaves spec untouched", func(t *testing.T) { + t.Parallel() + strategy := generic.NewStrategy(runtime.NewScheme(), gv) + obj := obj.DeepCopy() + originalSpec := *obj.Spec.DeepCopy() + + strategy.PrepareForCreate(t.Context(), obj) + require.Equal(t, originalSpec, obj.Spec) + }) + }) } -func TestPrepareForCreate(t *testing.T) { - ctx := context.TODO() +func TestStatusStrategy(t *testing.T) { + t.Parallel() gv := schema.GroupVersion{Group: "test", Version: "v1"} - strategy := generic.NewStrategy(runtime.NewScheme(), gv) - obj := &example.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: example.PodSpec{ - NodeSelector: map[string]string{"foo": "bar"}, - }, - Status: example.PodStatus{ - Phase: example.PodPhase("Running"), - }, - } + t.Run("PrepareForUpdate", func(t *testing.T) { + t.Parallel() - strategy.PrepareForCreate(ctx, obj) - require.Equal(t, int64(1), obj.Generation) - require.Equal(t, example.PodStatus{}, obj.Status) + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Generation: 1, + }, + Spec: example.PodSpec{ + NodeSelector: map[string]string{"foo": "bar"}, + }, + Status: example.PodStatus{ + Phase: example.PodPhase("Running"), + }, + } + + t.Run("ignores spec updates", func(t *testing.T) { + // The assumption here is that the status strategy should not allow for spec updates. + // This is drawn due to the GetResetFields function returning `metadata` and `spec`, and due to it copying old `metadata` fields to the new object (but not spec?). + t.Skip("assumption does not hold -- verify with app platform if this is intended") + + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Spec.NodeSelector = map[string]string{"foo": "baz"} + expectedObj := obj.DeepCopy() + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + + t.Run("ignores label updates", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Labels = map[string]string{"foo": "baz"} + expectedObj := obj.DeepCopy() + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + + t.Run("ignores annotation updates", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Annotations = map[string]string{"foo": "baz"} + expectedObj := obj.DeepCopy() + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + + t.Run("ignores finalizer updates", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Finalizers = []string{"foo"} + expectedObj := obj.DeepCopy() + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + + t.Run("ignores owner references updates", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: "foo"}} + expectedObj := obj.DeepCopy() + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, expectedObj, newObj) + }) + + t.Run("does not increment generation on status changes", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Status.Phase = example.PodPhase("Stopped") + + strategy := generic.NewStatusStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, int64(1), newObj.Generation) + }) + }) +} + +func TestCompleteStrategy(t *testing.T) { + t.Parallel() + gv := schema.GroupVersion{Group: "test", Version: "v1"} + + t.Run("PrepareForUpdate", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Generation: 1, + }, + Spec: example.PodSpec{ + NodeSelector: map[string]string{"foo": "bar"}, + }, + Status: example.PodStatus{ + Phase: example.PodPhase("Running"), + }, + } + + t.Run("on status updates", func(t *testing.T) { + t.Parallel() + + t.Run("keeps the change", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Status.Phase = example.PodPhase("Stopped") + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, example.PodPhase("Stopped"), newObj.Status.Phase) + }) + + t.Run("does not change generation", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Status.Phase = example.PodPhase("Stopped") + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, int64(1), newObj.Generation) + }) + }) + + t.Run("on spec updates", func(t *testing.T) { + t.Parallel() + + t.Run("keeps the change", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Spec.NodeSelector = map[string]string{"foo": "baz"} + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, map[string]string{"foo": "baz"}, newObj.Spec.NodeSelector) + }) + + t.Run("does not increment generation", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.Spec.NodeSelector = map[string]string{"foo": "baz"} + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + require.Equal(t, int64(1), newObj.Generation) + }) + }) + + t.Run("on metadata updates", func(t *testing.T) { + t.Parallel() + + t.Run("keeps the change", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Annotations = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Labels = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Finalizers = []string{"foo"} + newObj.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: "foo"}} + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + assert.Equal(t, map[string]string{"foo": "baz"}, newObj.ObjectMeta.Annotations) + assert.Equal(t, map[string]string{"foo": "baz"}, newObj.ObjectMeta.Labels) + assert.Equal(t, []string{"foo"}, newObj.ObjectMeta.Finalizers) + assert.Equal(t, []metav1.OwnerReference{{Name: "foo"}}, newObj.ObjectMeta.OwnerReferences) + }) + + t.Run("does not increment generation", func(t *testing.T) { + t.Parallel() + oldObj := obj.DeepCopy() + newObj := obj.DeepCopy() + newObj.ObjectMeta.Annotations = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Labels = map[string]string{"foo": "baz"} + newObj.ObjectMeta.Finalizers = []string{"foo"} + newObj.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: "foo"}} + + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + strategy.PrepareForUpdate(t.Context(), newObj, oldObj) + assert.Equal(t, int64(1), newObj.Generation) + }) + }) + }) + + t.Run("PrepareForCreate", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: example.PodSpec{ + NodeSelector: map[string]string{"foo": "bar"}, + }, + Status: example.PodStatus{ + Phase: example.PodPhase("Running"), + }, + } + + t.Run("assigns generation=1", func(t *testing.T) { + t.Parallel() + + t.Run("when generation is not set", func(t *testing.T) { + t.Parallel() + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + obj := obj.DeepCopy() + + strategy.PrepareForCreate(t.Context(), obj) + require.Equal(t, int64(1), obj.Generation) + }) + + t.Run("when generation is set to a higher value", func(t *testing.T) { + t.Parallel() + strategy := generic.NewCompleteStrategy(runtime.NewScheme(), gv) + obj := obj.DeepCopy() + obj.Generation = 2 + + strategy.PrepareForCreate(t.Context(), obj) + require.Equal(t, int64(1), obj.Generation) + }) + }) + }) +} + +func TestGetAttrs(t *testing.T) { + t.Parallel() + + t.Run("returns all labels", func(t *testing.T) { + t.Parallel() + + t.Run("when labels is nil", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: nil, + }, + } + + labels, _, err := generic.GetAttrs(obj) + require.NoError(t, err) + + require.Empty(t, labels, "expected no labels") + }) + + t.Run("when there are no labels", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: make(map[string]string), + }, + } + + labels, _, err := generic.GetAttrs(obj) + require.NoError(t, err) + + require.Empty(t, labels, "expected no labels") + }) + + t.Run("when there is only 1 label", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: map[string]string{"foo": "bar"}, + }, + } + + l, _, err := generic.GetAttrs(obj) + require.NoError(t, err) + + require.Equal(t, labels.Set{"foo": "bar"}, l, "expected labels to match") + }) + + t.Run("when there are many labels", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: map[string]string{"foo": "bar", "baz": "qux", "grafana": "is-cool"}, + }, + } + + l, _, err := generic.GetAttrs(obj) + require.NoError(t, err) + + require.Equal(t, labels.Set{"foo": "bar", "baz": "qux", "grafana": "is-cool"}, l, "expected labels to match") + }) + }) + + t.Run("includes only name in fields", func(t *testing.T) { + t.Parallel() + + obj := &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + _, f, err := generic.GetAttrs(obj) + require.NoError(t, err) + + require.Equal(t, fields.Set{"metadata.name": "test"}, f, "expected fields to match") + }) } diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 14352adba3e..380ae46e2c0 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -10,7 +10,7 @@ replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible require ( cloud.google.com/go/storage v1.50.0 // @grafana/grafana-backend-group github.com/Masterminds/semver/v3 v3.3.0 // @grafana/grafana-developer-enablement-squad - github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources + github.com/aws/aws-sdk-go v1.55.6 // @grafana/aws-datasources github.com/docker/docker v27.5.1+incompatible // @grafana/grafana-developer-enablement-squad github.com/drone/drone-cli v1.8.0 // @grafana/grafana-developer-enablement-squad github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend @@ -34,7 +34,7 @@ require ( golang.org/x/text v0.22.0 // indirect; @grafana/grafana-backend-group golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.220.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.71.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.5 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) @@ -72,10 +72,10 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect; @grafana/grafana-backend-group - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -93,10 +93,10 @@ require ( github.com/adrg/xdg v0.4.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -106,7 +106,7 @@ require ( github.com/sosodev/duration v1.2.0 // indirect github.com/vektah/gqlparser/v2 v2.5.20 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 1d55ac2a88b..8428aa1138b 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -53,8 +53,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= @@ -70,8 +70,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= @@ -110,8 +110,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= -github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -251,8 +251,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= @@ -335,8 +335,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -366,17 +366,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/expr/convert_from_full_long.go b/pkg/expr/convert_from_full_long.go new file mode 100644 index 00000000000..c4de09cbee3 --- /dev/null +++ b/pkg/expr/convert_from_full_long.go @@ -0,0 +1,128 @@ +package expr + +import ( + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func ConvertFromFullLongToNumericMulti(frames data.Frames) (data.Frames, error) { + if len(frames) != 1 { + return nil, fmt.Errorf("expected exactly one frame, got %d", len(frames)) + } + frame := frames[0] + if frame.Meta == nil || frame.Meta.Type != numericFullLongType { + return nil, fmt.Errorf("expected frame of type %q", numericFullLongType) + } + + var ( + metricField *data.Field + valueField *data.Field + displayField *data.Field + labelFields []*data.Field + ) + + // Identify key fields + for _, f := range frame.Fields { + switch f.Name { + case SQLMetricFieldName: + metricField = f + case SQLValueFieldName: + valueField = f + case SQLDisplayFieldName: + displayField = f + default: + if f.Type() == data.FieldTypeNullableString { + labelFields = append(labelFields, f) + } + } + } + + if metricField == nil || valueField == nil { + return nil, fmt.Errorf("missing required fields: %q or %q", SQLMetricFieldName, SQLValueFieldName) + } + + type seriesKey struct { + metric string + labelFP data.Fingerprint + displayName string + } + + type seriesEntry struct { + indices []int + labels data.Labels + displayName *string + } + + grouped := make(map[seriesKey]*seriesEntry) + + for i := 0; i < frame.Rows(); i++ { + if valueField.NilAt(i) { + continue // skip null values + } + + metric := metricField.At(i).(string) + + // collect labels + labels := data.Labels{} + for _, f := range labelFields { + if f.NilAt(i) { + continue + } + val := f.At(i).(*string) + if val != nil { + labels[f.Name] = *val + } + } + fp := labels.Fingerprint() + + // handle optional display name + var displayPtr *string + displayKey := "" + if displayField != nil && !displayField.NilAt(i) { + if raw := displayField.At(i).(*string); raw != nil { + displayPtr = raw + displayKey = *raw + } + } + + key := seriesKey{ + metric: metric, + labelFP: fp, + displayName: displayKey, + } + + entry, ok := grouped[key] + if !ok { + entry = &seriesEntry{ + labels: labels, + displayName: displayPtr, + } + grouped[key] = entry + } + entry.indices = append(entry.indices, i) + } + + var result data.Frames + for key, entry := range grouped { + values := make([]*float64, 0, len(entry.indices)) + for _, i := range entry.indices { + v, err := valueField.FloatAt(i) + if err != nil { + return nil, fmt.Errorf("failed to convert value at index %d to float: %w", i, err) + } + values = append(values, &v) + } + + field := data.NewField(key.metric, entry.labels, values) + if entry.displayName != nil { + field.Config = &data.FieldConfig{DisplayNameFromDS: *entry.displayName} + } + + frame := data.NewFrame("", field) + frame.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + result = append(result, frame) + } + + return result, nil +} diff --git a/pkg/expr/convert_from_full_long_test.go b/pkg/expr/convert_from_full_long_test.go new file mode 100644 index 00000000000..2487fac661d --- /dev/null +++ b/pkg/expr/convert_from_full_long_test.go @@ -0,0 +1,192 @@ +package expr + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" +) + +func TestConvertFromFullLongToNumericMulti(t *testing.T) { + t.Run("SingleRowNoLabels", func(t *testing.T) { + input := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), + ) + input.Meta = &data.FrameMeta{Type: numericFullLongType} + + out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) + require.NoError(t, err) + require.Len(t, out, 1) + + expected := data.NewFrame("", + data.NewField("cpu", nil, []*float64{fp(3.14)}), + ) + expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoRowsWithLabelsAndDisplay", func(t *testing.T) { + input := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU A"), sp("CPU A")}), + data.NewField("host", nil, []*string{sp("a"), sp("a")}), + ) + input.Meta = &data.FrameMeta{Type: numericFullLongType} + + out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) + require.NoError(t, err) + require.Len(t, out, 1) + + expected := data.NewFrame("", + func() *data.Field { + f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0), fp(2.0)}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} + return f + }(), + ) + expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("SkipsNullValues", func(t *testing.T) { + input := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), nil}), + ) + input.Meta = &data.FrameMeta{Type: numericFullLongType} + + out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) + require.NoError(t, err) + require.Len(t, out, 1) + + expected := data.NewFrame("", + data.NewField("cpu", nil, []*float64{fp(1.0)}), + ) + expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) +} + +func TestConvertNumericMultiRoundTripToFullLongAndBack(t *testing.T) { + t.Run("TwoFieldsWithSparseLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}), + ), + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []*float64{fp(2.0)}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + fullLong, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, fullLong, 1) + + roundTrip, err := ConvertFromFullLongToNumericMulti(fullLong) + require.NoError(t, err) + + expected := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}), + ), + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []*float64{fp(2.0)}), + ), + } + for _, f := range expected { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + sortFramesByMetricDisplayAndLabels(expected) + sortFramesByMetricDisplayAndLabels(roundTrip) + + require.Len(t, roundTrip, len(expected)) + for i := range expected { + if diff := cmp.Diff(expected[i], roundTrip[i], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Mismatch on frame %d (-want +got):\n%s", i, diff) + } + } + }) + + t.Run("PreservesDisplayName", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + func() *data.Field { + f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} + return f + }(), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + fullLong, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, fullLong, 1) + + roundTrip, err := ConvertFromFullLongToNumericMulti(fullLong) + require.NoError(t, err) + + expected := data.Frames{ + data.NewFrame("", + func() *data.Field { + f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} + return f + }(), + ), + } + expected[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + sortFramesByMetricDisplayAndLabels(expected) + sortFramesByMetricDisplayAndLabels(roundTrip) + + require.Len(t, roundTrip, 1) + if diff := cmp.Diff(expected[0], roundTrip[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Mismatch (-want +got):\n%s", diff) + } + }) +} + +func sortFramesByMetricDisplayAndLabels(frames data.Frames) { + sort.Slice(frames, func(i, j int) bool { + fi := frames[i].Fields[0] + fj := frames[j].Fields[0] + + // 1. Metric name + if fi.Name != fj.Name { + return fi.Name < fj.Name + } + + // 2. Display name (if set) + var di, dj string + if fi.Config != nil { + di = fi.Config.DisplayNameFromDS + } + if fj.Config != nil { + dj = fj.Config.DisplayNameFromDS + } + if di != dj { + return di < dj + } + + // 3. Labels fingerprint + return fi.Labels.Fingerprint() < fj.Labels.Fingerprint() + }) +} diff --git a/pkg/expr/convert_to_full_long.go b/pkg/expr/convert_to_full_long.go new file mode 100644 index 00000000000..94dc2648bd6 --- /dev/null +++ b/pkg/expr/convert_to_full_long.go @@ -0,0 +1,400 @@ +package expr + +import ( + "fmt" + "sort" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +const ( + SQLMetricFieldName = "__metric_name__" + SQLValueFieldName = "__value__" + SQLDisplayFieldName = "__display_name__" + + // These are not types in the SDK or dataplane contract yet. + numericFullLongType = "numeric_full_long" + timeseriesFullLongType = "time_series_full_long" +) + +func ConvertToFullLong(frames data.Frames) (data.Frames, error) { + if len(frames) == 0 { + return frames, nil + } + + var inputType data.FrameType + if frames[0].Meta != nil && frames[0].Meta.Type != "" { + inputType = frames[0].Meta.Type + } else { + return nil, fmt.Errorf("input frame missing FrameMeta.Type") + } + + if !supportedToLongConversion(inputType) { + return nil, fmt.Errorf("unsupported input dataframe type %s for full long conversion", inputType) + } + + switch inputType { + case data.FrameTypeNumericMulti: + return convertNumericMultiToFullLong(frames) + case data.FrameTypeNumericWide: + return convertNumericWideToFullLong(frames) + case data.FrameTypeTimeSeriesMulti: + return convertTimeSeriesMultiToFullLong(frames) + case data.FrameTypeTimeSeriesWide: + return convertTimeSeriesWideToFullLong(frames) + default: + return nil, fmt.Errorf("unsupported input type %s for full long conversion", inputType) + } +} + +func convertNumericMultiToFullLong(frames data.Frames) (data.Frames, error) { + wide := convertNumericMultiToNumericWide(frames) + return convertNumericWideToFullLong(wide) +} + +func convertNumericWideToFullLong(frames data.Frames) (data.Frames, error) { + if len(frames) != 1 { + return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) + } + inputFrame := frames[0] + if inputFrame.Rows() > 1 { + return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows()) + } + + var ( + metricCol = make([]string, 0, len(inputFrame.Fields)) + valueCol = make([]*float64, 0, len(inputFrame.Fields)) + displayCol = make([]*string, 0, len(inputFrame.Fields)) + hasDisplayCol bool + ) + + labelKeySet := map[string]struct{}{} + for _, field := range inputFrame.Fields { + if !field.Type().Numeric() { + continue + } + val, err := field.FloatAt(0) + if err != nil { + continue + } + v := val + valueCol = append(valueCol, &v) + metricCol = append(metricCol, field.Name) + + // Display name + var d *string + if field.Config != nil && field.Config.DisplayNameFromDS != "" { + s := field.Config.DisplayNameFromDS + d = &s + hasDisplayCol = true + } + displayCol = append(displayCol, d) + + for k := range field.Labels { + labelKeySet[k] = struct{}{} + } + } + + labelKeys := make([]string, 0, len(labelKeySet)) + + labelValues := make(map[string][]*string) + for k := range labelKeySet { + labelKeys = append(labelKeys, k) + labelValues[k] = make([]*string, 0, len(valueCol)) + } + sort.Strings(labelKeys) + + for _, field := range inputFrame.Fields { + if !field.Type().Numeric() { + continue + } + for _, k := range labelKeys { + var val *string + if field.Labels != nil { + if v, ok := field.Labels[k]; ok { + val = &v + } + } + labelValues[k] = append(labelValues[k], val) + } + } + + fields := []*data.Field{ + data.NewField(SQLMetricFieldName, nil, metricCol), + data.NewField(SQLValueFieldName, nil, valueCol), + } + if hasDisplayCol { + fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displayCol)) + } + for _, k := range labelKeys { + fields = append(fields, data.NewField(k, nil, labelValues[k])) + } + + out := data.NewFrame("", fields...) + out.Meta = &data.FrameMeta{Type: numericFullLongType} + return data.Frames{out}, nil +} + +func convertTimeSeriesMultiToFullLong(frames data.Frames) (data.Frames, error) { + type row struct { + t time.Time + value *float64 + metric string + display *string + labels data.Labels + } + + var rows []row + labelKeysSet := map[string]struct{}{} + hasDisplayCol := false + + for _, frame := range frames { + var timeField *data.Field + for _, f := range frame.Fields { + if f.Type() == data.FieldTypeTime { + timeField = f + break + } + } + if timeField == nil { + return nil, fmt.Errorf("missing time field") + } + for _, f := range frame.Fields { + if !f.Type().Numeric() { + continue + } + var display *string + if f.Config != nil && f.Config.DisplayNameFromDS != "" { + s := f.Config.DisplayNameFromDS + display = &s + hasDisplayCol = true + } + for i := 0; i < f.Len(); i++ { + t := timeField.At(i).(time.Time) + v, err := f.FloatAt(i) + if err != nil { + continue + } + val := v + rows = append(rows, row{ + t: t, + value: &val, + metric: f.Name, + display: display, + labels: f.Labels, + }) + for k := range f.Labels { + labelKeysSet[k] = struct{}{} + } + } + } + } + + labelKeys := make([]string, 0, len(labelKeysSet)) + for k := range labelKeysSet { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + sort.SliceStable(rows, func(i, j int) bool { + if rows[i].t.Equal(rows[j].t) { + return rows[i].metric < rows[j].metric + } + return rows[i].t.Before(rows[j].t) + }) + + times := make([]time.Time, len(rows)) + values := make([]*float64, len(rows)) + metrics := make([]string, len(rows)) + var displays []*string + if hasDisplayCol { + displays = make([]*string, len(rows)) + } + labels := make(map[string][]*string) + for _, k := range labelKeys { + labels[k] = make([]*string, len(rows)) + } + + for i, r := range rows { + times[i] = r.t + values[i] = r.value + metrics[i] = r.metric + if hasDisplayCol { + displays[i] = r.display + } + for _, k := range labelKeys { + if v, ok := r.labels[k]; ok { + labels[k][i] = &v + } + } + } + + fields := []*data.Field{ + data.NewField("time", nil, times), + data.NewField(SQLValueFieldName, nil, values), + data.NewField(SQLMetricFieldName, nil, metrics), + } + if hasDisplayCol { + fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays)) + } + for _, k := range labelKeys { + fields = append(fields, data.NewField(k, nil, labels[k])) + } + + out := data.NewFrame("", fields...) + out.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + return data.Frames{out}, nil +} + +func convertTimeSeriesWideToFullLong(frames data.Frames) (data.Frames, error) { + if len(frames) != 1 { + return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) + } + frame := frames[0] + + var timeField *data.Field + for _, f := range frame.Fields { + if f.Type() == data.FieldTypeTime { + timeField = f + break + } + } + if timeField == nil { + return nil, fmt.Errorf("time field not found in TimeSeriesWide frame") + } + + type row struct { + t time.Time + value *float64 + metric string + display *string + labels data.Labels + } + + var ( + rows []row + labelKeysSet = map[string]struct{}{} + hasDisplayCol bool + ) + + // Collect all label keys + for _, f := range frame.Fields { + if !f.Type().Numeric() { + continue + } + for k := range f.Labels { + labelKeysSet[k] = struct{}{} + } + } + + labelKeys := make([]string, 0, len(labelKeysSet)) + for k := range labelKeysSet { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + + timeLen := timeField.Len() + for _, f := range frame.Fields { + if !f.Type().Numeric() { + continue + } + var display *string + if f.Config != nil && f.Config.DisplayNameFromDS != "" { + s := f.Config.DisplayNameFromDS + display = &s + hasDisplayCol = true + } + for i := 0; i < timeLen; i++ { + t := timeField.At(i).(time.Time) + v, err := f.FloatAt(i) + if err != nil { + continue + } + val := v + rows = append(rows, row{ + t: t, + value: &val, + metric: f.Name, + display: display, + labels: f.Labels, + }) + } + } + + sort.SliceStable(rows, func(i, j int) bool { + if rows[i].t.Equal(rows[j].t) { + return rows[i].metric < rows[j].metric + } + return rows[i].t.Before(rows[j].t) + }) + + times := make([]time.Time, len(rows)) + values := make([]*float64, len(rows)) + metrics := make([]string, len(rows)) + var displays []*string + if hasDisplayCol { + displays = make([]*string, len(rows)) + } + labels := make(map[string][]*string) + for _, k := range labelKeys { + labels[k] = make([]*string, len(rows)) + } + + for i, r := range rows { + times[i] = r.t + values[i] = r.value + metrics[i] = r.metric + if hasDisplayCol { + displays[i] = r.display + } + for _, k := range labelKeys { + if v, ok := r.labels[k]; ok { + labels[k][i] = &v + } + } + } + + fields := []*data.Field{ + data.NewField("time", nil, times), + data.NewField(SQLValueFieldName, nil, values), + data.NewField(SQLMetricFieldName, nil, metrics), + } + if hasDisplayCol { + fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays)) + } + for _, k := range labelKeys { + fields = append(fields, data.NewField(k, nil, labels[k])) + } + + out := data.NewFrame("", fields...) + out.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + return data.Frames{out}, nil +} + +func supportedToLongConversion(inputType data.FrameType) bool { + switch inputType { + case data.FrameTypeNumericMulti, data.FrameTypeNumericWide: + return true + case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide: + return true + default: + return false + } +} + +func convertNumericMultiToNumericWide(frames data.Frames) data.Frames { + if len(frames) == 0 { + return nil + } + + out := data.NewFrame("") + for _, frame := range frames { + for _, field := range frame.Fields { + if field.Type().Numeric() { + out.Fields = append(out.Fields, field) + } + } + } + out.Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + return data.Frames{out} +} diff --git a/pkg/expr/convert_to_full_long_num_test.go b/pkg/expr/convert_to_full_long_num_test.go new file mode 100644 index 00000000000..88a157f89aa --- /dev/null +++ b/pkg/expr/convert_to_full_long_num_test.go @@ -0,0 +1,507 @@ +package expr + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" +) + +func TestConvertNumericWideToFullLong(t *testing.T) { + t.Run("SingleItemNoLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", nil, []float64{3.14}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("MultiRowShouldError", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", nil, []float64{1.0, 2.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + _, err := ConvertToFullLong(input) + require.Error(t, err) + require.Contains(t, err.Error(), "no more than one row") + }) + + t.Run("TwoItemsWithSingleLabel", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField("host", nil, []*string{sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoItemsWithSparseLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{2.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField("env", nil, []*string{nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoDifferentMetricsWithSharedLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + data.NewField("mem", data.Labels{"host": "a"}, []float64{4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), + data.NewField("host", nil, []*string{sp("a"), sp("a")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSparseMetricsAndLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + data.NewField("mem", data.Labels{"env": "prod"}, []float64{4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), + data.NewField("env", nil, []*string{nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), nil}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("ExtraTimeFieldIsDropped", func(t *testing.T) { + // Note we may consider changing this behavior and looking into keeping + // remainder fields in the future. + input := data.Frames{ + data.NewFrame("numeric", + data.NewField("timestamp", nil, []time.Time{time.Now()}), // extra time field + data.NewField("cpu", nil, []float64{1.23}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.23)}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) +} + +func TestConvertNumericWideToFullLongWithDisplayName(t *testing.T) { + t.Run("SingleFieldWithDisplayName", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + func() *data.Field { + f := data.NewField("cpu", nil, []float64{3.14}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU Display"} + return f + }(), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), + data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU Display")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("MixedDisplayNames", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("numeric", + func() *data.Field { + f := data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} + return f + }(), + data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU A"), nil}), + data.NewField("host", nil, []*string{sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) +} + +func TestConvertNumericMultiToFullLong(t *testing.T) { + t.Run("SingleItemNoLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", nil, []float64{3.14}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoItemsWithSingleLabel", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + ), + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField("host", nil, []*string{sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoItemsWithSparseLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + ), + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{2.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField("env", nil, []*string{nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoDifferentMetricsWithSharedLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + ), + data.NewFrame("", + data.NewField("mem", data.Labels{"host": "a"}, []float64{4.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), + data.NewField("host", nil, []*string{sp("a"), sp("a")}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSparseMetricsAndLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), + ), + data.NewFrame("", + data.NewField("mem", data.Labels{"env": "prod"}, []float64{4.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} + } + + expected := data.NewFrame("", + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), + data.NewField("env", nil, []*string{nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), nil}), + ) + expected.Meta = &data.FrameMeta{Type: numericFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) +} + +func TestConvertTimeSeriesWideToFullLong(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + t.Run("SingleSeriesNoLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("time", nil, times), + data.NewField("cpu", nil, []float64{1.0, 2.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} + + expected := data.NewFrame("", + data.NewField("time", nil, times), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoSeriesOneLabel", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoMetricsWithSharedLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + data.NewField("mem", data.Labels{"host": "a"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), + data.NewField("host", nil, []*string{sp("a"), sp("a"), sp("a"), sp("a")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoSeriesSparseLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), + data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoSeriesSparseMetricsAndLabels", func(t *testing.T) { + input := data.Frames{ + data.NewFrame("", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), + data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) +} + +func sp(s string) *string { + return &s +} diff --git a/pkg/expr/convert_to_full_long_ts_test.go b/pkg/expr/convert_to_full_long_ts_test.go new file mode 100644 index 00000000000..eb43d9324fe --- /dev/null +++ b/pkg/expr/convert_to_full_long_ts_test.go @@ -0,0 +1,373 @@ +package expr + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" +) + +func TestConvertTimeSeriesMultiToFullLong(t *testing.T) { + t.Run("SingleSeriesNoLabels", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + time.Unix(20, 0), + } + values := []float64{1.0, 2.0, 3.0} + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", nil, values), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, times), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0), fp(3.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu"}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSeriesOneLabel", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), + }), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoMetricsWithSharedLabels", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("mem", + data.NewField("time", nil, times), + data.NewField("mem", data.Labels{"host": "a"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), + }), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), + data.NewField("host", nil, []*string{sp("a"), sp("a"), sp("a"), sp("a")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSeriesSparseLabels", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), + }), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), + data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSeriesSparseMetrics", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("mem", + data.NewField("time", nil, times), + data.NewField("mem", data.Labels{"host": "b"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), + }), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("TwoSeriesSparseMetricsAndLabels", func(t *testing.T) { + times := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("mem", + data.NewField("time", nil, times), + data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), + }), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), + data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), + data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) + + t.Run("ThreeSeriesSparseTimeLabelsMetrics", func(t *testing.T) { + timesA := []time.Time{ + time.Unix(0, 0), + time.Unix(10, 0), + } + timesB := []time.Time{ + time.Unix(5, 0), + time.Unix(15, 0), + } + timesMem := []time.Time{ + time.Unix(10, 0), + time.Unix(30, 0), + } + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, timesA), + data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), + ), + data.NewFrame("cpu", + data.NewField("time", nil, timesB), + data.NewField("cpu", nil, []float64{9.0, 10.0}), // no labels + ), + data.NewFrame("mem", + data.NewField("time", nil, timesMem), + data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + } + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + time.Unix(0, 0), // cpu a + time.Unix(5, 0), // cpu no label + time.Unix(10, 0), // cpu a + time.Unix(10, 0), // mem + time.Unix(15, 0), // cpu no label + time.Unix(30, 0), // mem + }), + data.NewField(SQLValueFieldName, nil, []*float64{ + fp(1.0), fp(9.0), fp(2.0), fp(3.0), fp(10.0), fp(4.0), + }), + data.NewField(SQLMetricFieldName, nil, []string{ + "cpu", "cpu", "cpu", "mem", "cpu", "mem", + }), + data.NewField("env", nil, []*string{ + nil, nil, nil, sp("prod"), nil, sp("prod"), + }), + data.NewField("host", nil, []*string{ + sp("a"), nil, sp("a"), sp("b"), nil, sp("b"), + }), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s", diff) + } + }) +} + +func TestConvertTimeSeriesMultiToFullLongWithDisplayName(t *testing.T) { + t.Run("SingleSeriesWithDisplayName", func(t *testing.T) { + times := []time.Time{time.Unix(0, 0), time.Unix(10, 0)} + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + func() *data.Field { + f := data.NewField("cpu", nil, []float64{1.0, 2.0}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU Display"} + return f + }(), + ), + } + input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + + expected := data.NewFrame("", + data.NewField("time", nil, times), + data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), + data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU Display"), sp("CPU Display")}), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("TwoSeriesMixedDisplayNames", func(t *testing.T) { + times := []time.Time{time.Unix(0, 0), time.Unix(10, 0)} + + input := data.Frames{ + data.NewFrame("cpu", + data.NewField("time", nil, times), + func() *data.Field { + f := data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}) + f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} + return f + }(), + ), + data.NewFrame("cpu", + data.NewField("time", nil, times), + data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), + ), + } + for _, f := range input { + f.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} + } + + expected := data.NewFrame("", + data.NewField("time", nil, []time.Time{ + times[0], times[0], times[1], times[1], + }), + data.NewField(SQLValueFieldName, nil, []*float64{ + fp(1.0), fp(3.0), fp(2.0), fp(4.0), + }), + data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), + data.NewField(SQLDisplayFieldName, nil, []*string{ + sp("CPU A"), nil, sp("CPU A"), nil, + }), + data.NewField("host", nil, []*string{ + sp("a"), sp("b"), sp("a"), sp("b"), + }), + ) + expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} + + output, err := ConvertToFullLong(input) + require.NoError(t, err) + require.Len(t, output, 1) + if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) + } + }) +} diff --git a/pkg/expr/convert_to_long.go b/pkg/expr/convert_to_long.go deleted file mode 100644 index 3ab3339b8b0..00000000000 --- a/pkg/expr/convert_to_long.go +++ /dev/null @@ -1,311 +0,0 @@ -package expr - -import ( - "fmt" - "sort" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/data" -) - -func ConvertToLong(frames data.Frames) (data.Frames, error) { - if len(frames) == 0 { - // general empty case for now - return frames, nil - } - // Four Conversion Possible Cases - // 1. NumericMulti -> NumericLong - // 2. NumericWide -> NumericLong - // 3. TimeSeriesMulti -> TimeSeriesLong - // 4. TimeSeriesWide -> TimeSeriesLong - - // Detect if input type is declared - // First Check Frame Meta Type - - var inputType data.FrameType - if frames[0].Meta != nil && frames[0].Meta.Type != "" { - inputType = frames[0].Meta.Type - } - - // TODO: Add some guessing of Type if not declared - if inputType == "" { - return frames, fmt.Errorf("no input dataframe type set") - } - - if !supportedToLongConversion(inputType) { - return frames, fmt.Errorf("unsupported input dataframe type %s for SQL expression", inputType) - } - - toLong := getToLongConversionFunc(inputType) - if toLong == nil { - return frames, fmt.Errorf("could not get conversion function for input type %s", inputType) - } - - return toLong(frames) -} - -func convertNumericMultiToNumericLong(frames data.Frames) (data.Frames, error) { - // Apart from metadata, NumericMulti is basically NumericWide, except one frame per thing - // so we collapse into wide and call the wide conversion - wide := convertNumericMultiToNumericWide(frames) - return convertNumericWideToNumericLong(wide) -} - -func convertNumericMultiToNumericWide(frames data.Frames) data.Frames { - newFrame := data.NewFrame("") - for _, frame := range frames { - for _, field := range frame.Fields { - if !field.Type().Numeric() { - continue - } - newField := data.NewFieldFromFieldType(field.Type(), field.Len()) - newField.Name = field.Name - newField.Labels = field.Labels.Copy() - if field.Len() == 1 { - newField.Set(0, field.CopyAt(0)) - } - newFrame.Fields = append(newFrame.Fields, newField) - } - } - return data.Frames{newFrame} -} - -func convertNumericWideToNumericLong(frames data.Frames) (data.Frames, error) { - // Wide should only be one frame - if len(frames) != 1 { - return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) - } - inputFrame := frames[0] - - // The Frame should have no more than one row - if inputFrame.Rows() > 1 { - return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows()) - } - - // Gather: - // - unique numeric Field Names, and - // - unique Label Keys (from Numeric Fields only) - // each one maps to a field in the output long Frame. - uniqueNames := make([]string, 0) - uniqueKeys := make([]string, 0) - - uniqueNamesMap := make(map[string]data.FieldType) - uniqueKeysMap := make(map[string]struct{}) - - prints := make(map[string]int) - - registerPrint := func(labels data.Labels) { - fp := labels.Fingerprint().String() - if _, ok := prints[fp]; !ok { - prints[fp] = len(prints) - } - } - - for _, field := range inputFrame.Fields { - if field.Type().Numeric() { - if _, ok := uniqueNamesMap[field.Name]; !ok { - uniqueNames = append(uniqueNames, field.Name) - uniqueNamesMap[field.Name] = field.Type() - } - - if field.Labels != nil { - registerPrint(field.Labels) - for key := range field.Labels { - if _, ok := uniqueKeysMap[key]; !ok { - uniqueKeys = append(uniqueKeys, key) - } - uniqueKeysMap[key] = struct{}{} - } - } - } - } - - // Create new fields for output Long frame - fields := make([]*data.Field, 0, len(uniqueNames)+len(uniqueKeys)) - - // Create the Numeric Fields, tracking the index of each field by name - // Note: May want to use FloatAt and and prepopulate with NaN so missing - // combinations of value can be NA instead of the zero value of 0. - var nameIndexMap = make(map[string]int, len(uniqueNames)) - for i, name := range uniqueNames { - field := data.NewFieldFromFieldType(uniqueNamesMap[name], len(prints)) - field.Name = name - fields = append(fields, field) - nameIndexMap[name] = i - } - - // Create the String fields, tracking the index of each field by key - var keyIndexMap = make(map[string]int, len(uniqueKeys)) - for i, k := range uniqueKeys { - fields = append(fields, data.NewField(k, nil, make([]string, len(prints)))) - keyIndexMap[k] = len(nameIndexMap) + i - } - - longFrame := data.NewFrame("", fields...) - - if inputFrame.Rows() == 0 { - return data.Frames{longFrame}, nil - } - - // Add Rows to the fields - for _, field := range inputFrame.Fields { - if !field.Type().Numeric() { - continue - } - fieldIdx := prints[field.Labels.Fingerprint().String()] - longFrame.Fields[nameIndexMap[field.Name]].Set(fieldIdx, field.CopyAt(0)) - for key, value := range field.Labels { - longFrame.Fields[keyIndexMap[key]].Set(fieldIdx, value) - } - } - - return data.Frames{longFrame}, nil -} - -func convertTimeSeriesMultiToTimeSeriesLong(frames data.Frames) (data.Frames, error) { - // Collect all time values and ensure no duplicates - timeSet := make(map[time.Time]struct{}) - labelKeys := make(map[string]struct{}) // Collect all unique label keys - numericFields := make(map[string]struct{}) // Collect unique numeric field names - - for _, frame := range frames { - for _, field := range frame.Fields { - if field.Type() == data.FieldTypeTime { - for i := 0; i < field.Len(); i++ { - t := field.At(i).(time.Time) - timeSet[t] = struct{}{} - } - } else if field.Type().Numeric() { - numericFields[field.Name] = struct{}{} - if field.Labels != nil { - for key := range field.Labels { - labelKeys[key] = struct{}{} - } - } - } - } - } - - // Create a sorted slice of unique time values - times := make([]time.Time, 0, len(timeSet)) - for t := range timeSet { - times = append(times, t) - } - sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) }) - - // Create output fields: Time, one numeric field per unique numeric name, and label fields - timeField := data.NewField("Time", nil, times) - outputNumericFields := make(map[string]*data.Field) - for name := range numericFields { - outputNumericFields[name] = data.NewField(name, nil, make([]float64, len(times))) - } - outputLabelFields := make(map[string]*data.Field) - for key := range labelKeys { - outputLabelFields[key] = data.NewField(key, nil, make([]string, len(times))) - } - - // Map time to index for quick lookup - timeIndexMap := make(map[time.Time]int, len(times)) - for i, t := range times { - timeIndexMap[t] = i - } - - // Populate output fields - for _, frame := range frames { - var timeField *data.Field - for _, field := range frame.Fields { - if field.Type() == data.FieldTypeTime { - timeField = field - break - } - } - - if timeField == nil { - return nil, fmt.Errorf("no time field found in frame") - } - - for _, field := range frame.Fields { - if field.Type().Numeric() { - for i := 0; i < field.Len(); i++ { - t := timeField.At(i).(time.Time) - val, err := field.FloatAt(i) - if err != nil { - val = 0 // Default value for missing data - } - idx := timeIndexMap[t] - if outputField, exists := outputNumericFields[field.Name]; exists { - outputField.Set(idx, val) - } - - // Add labels for the numeric field - for key, value := range field.Labels { - if outputField, exists := outputLabelFields[key]; exists { - outputField.Set(idx, value) - } - } - } - } - } - } - - // Build the output frame - outputFields := []*data.Field{timeField} - for _, field := range outputNumericFields { - outputFields = append(outputFields, field) - } - for _, field := range outputLabelFields { - outputFields = append(outputFields, field) - } - outputFrame := data.NewFrame("time_series_long", outputFields...) - - // Set metadata - if outputFrame.Meta == nil { - outputFrame.Meta = &data.FrameMeta{} - } - outputFrame.Meta.Type = data.FrameTypeTimeSeriesLong - - return data.Frames{outputFrame}, nil -} - -func convertTimeSeriesWideToTimeSeriesLong(frames data.Frames) (data.Frames, error) { - // Wide should only be one frame - if len(frames) != 1 { - return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) - } - inputFrame := frames[0] - longFrame, err := data.WideToLong(inputFrame) - if err != nil { - return nil, fmt.Errorf("failed to convert wide time series to long timeseries for sql expression: %w", err) - } - return data.Frames{longFrame}, nil -} - -func getToLongConversionFunc(inputType data.FrameType) func(data.Frames) (data.Frames, error) { - switch inputType { - case data.FrameTypeNumericMulti: - return convertNumericMultiToNumericLong - case data.FrameTypeNumericWide: - return convertNumericWideToNumericLong - case data.FrameTypeTimeSeriesMulti: - return convertTimeSeriesMultiToTimeSeriesLong - case data.FrameTypeTimeSeriesWide: - return convertTimeSeriesWideToTimeSeriesLong - default: - return convertErr - } -} - -func convertErr(_ data.Frames) (data.Frames, error) { - return nil, fmt.Errorf("unsupported input type for SQL expression") -} - -func supportedToLongConversion(inputType data.FrameType) bool { - switch inputType { - case data.FrameTypeNumericMulti, data.FrameTypeNumericWide: - return true - case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide: - return true - default: - return false - } -} diff --git a/pkg/expr/convert_to_long_test.go b/pkg/expr/convert_to_long_test.go deleted file mode 100644 index 291fdb62f17..00000000000 --- a/pkg/expr/convert_to_long_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package expr - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/require" -) - -func TestConvertNumericMultiToLong(t *testing.T) { - input := data.Frames{ - data.NewFrame("test", - data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5})), - data.NewFrame("test", - data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), - ), - } - expectedFrame := data.NewFrame("", - data.NewField("Value", nil, []int64{5, 7}), - data.NewField("city", nil, []string{"MIA", "LGA"}), - ) - output, err := convertNumericMultiToNumericLong(input) - require.NoError(t, err) - - if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { - require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) - } -} - -func TestConvertNumericWideToLong(t *testing.T) { - input := data.Frames{ - data.NewFrame("test", - data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5}), - data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), - ), - } - expectedFrame := data.NewFrame("", - data.NewField("Value", nil, []int64{5, 7}), - data.NewField("city", nil, []string{"MIA", "LGA"}), - ) - output, err := convertNumericWideToNumericLong(input) - require.NoError(t, err) - - if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { - require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) - } -} diff --git a/pkg/expr/mathexp/parse/lex.go b/pkg/expr/mathexp/parse/lex.go index 7d74bd876ab..20dcb170e9c 100644 --- a/pkg/expr/mathexp/parse/lex.go +++ b/pkg/expr/mathexp/parse/lex.go @@ -67,13 +67,14 @@ type stateFn func(*lexer) stateFn // lexer holds the state of the scanner. type lexer struct { - input string // the string being scanned - state stateFn // the next lexing function to enter - pos Pos // current position in the input - start Pos // start position of this item - width Pos // width of last rune read from input - lastPos Pos // position of most recent item returned by nextItem - items chan item // channel of scanned items + input string // the string being scanned + state stateFn // the next lexing function to enter + pos Pos // current position in the input + start Pos // start position of this item + width Pos // width of last rune read from input + lastPos Pos // position of most recent item returned by nextItem + items chan item // channel of scanned items + done chan struct{} // channel to signal lexer shutdown } // next returns the next rune in the input. @@ -103,7 +104,11 @@ func (l *lexer) backup() { // emit passes an item back to the client. func (l *lexer) emit(t itemType) { - l.items <- item{t, l.start, l.input[l.start:l.pos]} + select { + case l.items <- item{t, l.start, l.input[l.start:l.pos]}: + case <-l.done: + return + } l.start = l.pos } @@ -139,7 +144,11 @@ func (l *lexer) lineNumber() int { // errorf returns an error token and terminates the scan by passing // back a nil pointer that will be the next state, terminating l.nextItem. func (l *lexer) errorf(format string, args ...any) stateFn { - l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} + select { + case l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}: + case <-l.done: + return nil + } return nil } @@ -155,15 +164,32 @@ func lex(input string) *lexer { l := &lexer{ input: input, items: make(chan item), + done: make(chan struct{}), } go l.run() return l } +// Close terminates the lexer goroutine. +func (l *lexer) Close() { + select { + case <-l.done: + // already closed + default: + close(l.done) + } +} + // run runs the state machine for the lexer. func (l *lexer) run() { + defer close(l.items) for l.state = lexItem; l.state != nil; { - l.state = l.state(l) + select { + case <-l.done: + return + default: + l.state = l.state(l) + } } } diff --git a/pkg/expr/mathexp/parse/lex_test.go b/pkg/expr/mathexp/parse/lex_test.go index e5b5ec59fc8..8356d2f9b3f 100644 --- a/pkg/expr/mathexp/parse/lex_test.go +++ b/pkg/expr/mathexp/parse/lex_test.go @@ -6,7 +6,9 @@ package parse import ( "fmt" + "runtime" "testing" + "time" ) // Make the types prettyprint. @@ -167,3 +169,70 @@ func TestLex(t *testing.T) { } } } + +// TestLexerClose verifies that a lexer can be explicitly closed +func TestLexerClose(t *testing.T) { + // Create a lexer with some input + lexer := lex("1 + 2") + + // Read one item to verify it's working + item := lexer.nextItem() + if item.typ != itemNumber || item.val != "1" { + t.Errorf("unexpected first item: %v", item) + } + + // Close the lexer explicitly + lexer.Close() + + // Verify the lexer's channel closes + select { + case _, ok := <-lexer.items: + if ok { + t.Fatal("lexer.items channel should be closed after lexer.Close()") + } + case <-time.After(100 * time.Millisecond): + t.Fatal("timed out waiting for lexer.items channel to close") + } +} + +// TestParseErrorNoLeak verifies that lexer goroutines are properly terminated when Parse encounters errors +func TestParseErrorNoLeak(t *testing.T) { + // Count initial goroutines + initialGoroutines := runtime.NumGoroutine() + + // Create several trees with parsing errors to check for leaks + for i := 0; i < 10; i++ { + tree := New() + input := "invalid expression with $" + err := tree.Parse(input) + + // Verify that Parse returned an error + if err == nil { + t.Fatal("expected error but got nil") + } + + // Verify that tree.lex is nil after an error + if tree.lex != nil { + t.Fatal("tree.lex was not set to nil after error") + } + } + + // Poll for goroutine count to stabilize + deadline := time.Now().Add(500 * time.Millisecond) + var finalGoroutines int + + for time.Now().Before(deadline) { + finalGoroutines = runtime.NumGoroutine() + // If we're close to the initial count, we can exit early + if finalGoroutines <= initialGoroutines+2 { + break + } + time.Sleep(10 * time.Millisecond) + } + + // Check if we've leaked goroutines (with a small buffer for normal variations) + if finalGoroutines > initialGoroutines+5 { + t.Fatalf("Goroutine leak detected: started with %d goroutines, ended with %d (difference of %d)", + initialGoroutines, finalGoroutines, finalGoroutines-initialGoroutines) + } +} diff --git a/pkg/expr/mathexp/parse/parse.go b/pkg/expr/mathexp/parse/parse.go index 4652390f29c..ed1af7e111a 100644 --- a/pkg/expr/mathexp/parse/parse.go +++ b/pkg/expr/mathexp/parse/parse.go @@ -140,7 +140,10 @@ func (t *Tree) startParse(funcs []map[string]Func, lex *lexer) { // stopParse terminates parsing. func (t *Tree) stopParse() { - t.lex = nil + if t.lex != nil { + t.lex.Close() + t.lex = nil + } } // Parse parses the expression definition string to construct a representation diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index dea3b10e659..141ce36e3af 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -429,7 +429,7 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s } if needsConversion { - convertedFrames, err := ConvertToLong(dataFrames) + convertedFrames, err := ConvertToFullLong(dataFrames) if err != nil { return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err) } diff --git a/pkg/expr/sql/db.go b/pkg/expr/sql/db.go index f1af425ad6d..a1badd44974 100644 --- a/pkg/expr/sql/db.go +++ b/pkg/expr/sql/db.go @@ -4,6 +4,7 @@ package sql import ( "context" + "fmt" sqle "github.com/dolthub/go-mysql-server" mysql "github.com/dolthub/go-mysql-server/sql" @@ -15,6 +16,42 @@ import ( // DB is a database that can execute SQL queries against a set of Frames. type DB struct{} +// GoMySQLServerError represents an error from the underlying Go MySQL Server +type GoMySQLServerError struct { + Err error +} + +// Error implements the error interface +func (e *GoMySQLServerError) Error() string { + return fmt.Sprintf("error in go-mysql-server: %v", e.Err) +} + +// Unwrap provides the original error for errors.Is/As +func (e *GoMySQLServerError) Unwrap() error { + return e.Err +} + +// WrapGoMySQLServerError wraps errors from Go MySQL Server with additional context +func WrapGoMySQLServerError(err error) error { + // Don't wrap nil errors + if err == nil { + return nil + } + + // Check if it's a function not found error or other specific GMS errors + if isFunctionNotFoundError(err) { + return &GoMySQLServerError{Err: err} + } + + // Return original error if it's not one we want to wrap + return err +} + +// isFunctionNotFoundError checks if the error is related to a function not being found +func isFunctionNotFoundError(err error) bool { + return mysql.ErrFunctionNotFound.Is(err) +} + // QueryFrames runs the sql query query against a database created from frames, and returns the frame. // The RefID of each frame becomes a table in the database. // It is expected that there is only one frame per RefID. @@ -47,7 +84,7 @@ func (db *DB) QueryFrames(ctx context.Context, name string, query string, frames schema, iter, _, err := engine.Query(mCtx, query) if err != nil { - return nil, err + return nil, WrapGoMySQLServerError(err) } f, err := convertToDataFrame(mCtx, iter, schema) diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go index 57171fbca94..77263d028de 100644 --- a/pkg/expr/sql/db_test.go +++ b/pkg/expr/sql/db_test.go @@ -193,6 +193,18 @@ func TestQueryFramesDateTimeSelect(t *testing.T) { } } +func TestErrorsFromGoMySQLServerAreFlagged(t *testing.T) { + const GmsNotImplemented = "STDDEV" // not implemented in go-mysql-server as of 2025-03-18 + + db := DB{} + + query := `SELECT ` + GmsNotImplemented + `(1);` + + _, err := db.QueryFrames(context.Background(), "sqlExpressionRefId", query, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "error in go-mysql-server") +} + // p is a utility for pointers from constants func p[T any](v T) *T { return &v diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go index 12269b4a68f..c65e4b89131 100644 --- a/pkg/expr/sql/parser.go +++ b/pkg/expr/sql/parser.go @@ -14,7 +14,7 @@ var logger = log.New("sql_expr") func TablesList(rawSQL string) ([]string, error) { stmt, err := sqlparser.Parse(rawSQL) if err != nil { - logger.Error("error parsing sql: %s", err.Error(), "sql", rawSQL) + logger.Error("error parsing sql", "error", err.Error(), "sql", rawSQL) return nil, fmt.Errorf("error parsing sql: %s", err.Error()) } diff --git a/pkg/expr/sql/parser_allow_test.go b/pkg/expr/sql/parser_allow_test.go index fc4b33369df..abb997d73c1 100644 --- a/pkg/expr/sql/parser_allow_test.go +++ b/pkg/expr/sql/parser_allow_test.go @@ -129,7 +129,17 @@ var example_case_statement = `SELECT END AS category FROM metrics` -var example_all_allowed_functions = `SELECT +var example_all_allowed_functions = `WITH sample_data AS ( + SELECT + 100 AS value, + 'example' AS name, + NOW() AS created_at + UNION ALL SELECT + 50 AS value, + 'test' AS name, + DATE_SUB(NOW(), INTERVAL 1 DAY) AS created_at +) +SELECT -- Conditional functions IF(value > 100, 'High', 'Low') AS conditional_if, COALESCE(value, 0) AS conditional_coalesce, @@ -191,6 +201,6 @@ var example_all_allowed_functions = `SELECT -- Type conversion CAST(value AS CHAR) AS type_cast, CONVERT(value, CHAR) AS type_convert -FROM metrics +FROM sample_data GROUP BY name, value, created_at LIMIT 10` diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index 0b4d7ab698e..8bcde6abb9b 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -14,6 +14,16 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" ) +var ( + ErrMissingSQLQuery = errutil.BadRequest("sql-missing-query").Errorf("missing SQL query") + ErrInvalidSQLQuery = errutil.BadRequest("sql-invalid-sql").MustTemplate( + "invalid SQL query: {{ .Private.query }} err: {{ .Error }}", + errutil.WithPublic( + "Invalid SQL query: {{ .Public.error }}", + ), + ) +) + // SQLCommand is an expression to run SQL over results type SQLCommand struct { query string @@ -25,15 +35,20 @@ type SQLCommand struct { // NewSQLCommand creates a new SQLCommand. func NewSQLCommand(refID, rawSQL string, limit int64) (*SQLCommand, error) { if rawSQL == "" { - return nil, errutil.BadRequest("sql-missing-query", - errutil.WithPublicMessage("missing SQL query")) + return nil, ErrMissingSQLQuery } tables, err := sql.TablesList(rawSQL) if err != nil { logger.Warn("invalid sql query", "sql", rawSQL, "error", err) - return nil, errutil.BadRequest("sql-invalid-sql", - errutil.WithPublicMessage(fmt.Sprintf("invalid SQL query: %s", err)), - ) + return nil, ErrInvalidSQLQuery.Build(errutil.TemplateData{ + Error: err, + Public: map[string]any{ + "error": err.Error(), + }, + Private: map[string]any{ + "query": rawSQL, + }, + }) } if len(tables) == 0 { logger.Warn("no tables found in SQL query", "sql", rawSQL) diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index 5f61d5d3847..ad2d58f3242 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -94,3 +94,11 @@ func IsTestDBMSSQL() bool { return false } + +func IsTestDBSpanner() bool { + if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { + return db == migrator.Spanner + } + + return false +} diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index cb30bd7f030..13ed55b238f 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -150,7 +150,7 @@ func (s *Service) collectSystemStats(ctx context.Context) (map[string]any, error m["stats.plugins.apps.count"] = s.appCount(ctx) m["stats.plugins.panels.count"] = s.panelCount(ctx) m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx) - m["stats.plugins.sandboxed_plugins.count"] = s.sandboxCount() + m["stats.plugins.sandboxed_plugins.count"] = s.sandboxCount(ctx) m["stats.alerts.count"] = statsResult.Alerts m["stats.active_users.count"] = statsResult.ActiveUsers m["stats.active_admins.count"] = statsResult.ActiveAdmins @@ -367,8 +367,8 @@ func (s *Service) dataSourceCount(ctx context.Context) int { return len(s.plugins.Plugins(ctx, plugins.TypeDataSource)) } -func (s *Service) sandboxCount() int { - ps, err := s.sandbox.Plugins() +func (s *Service) sandboxCount(ctx context.Context) int { + ps, err := s.sandbox.Plugins(ctx) if err != nil { s.log.Error("Failed to get sandboxed plugin count", "error", err) return 0 diff --git a/pkg/plugins/backendplugin/grpcplugin/client.go b/pkg/plugins/backendplugin/grpcplugin/client.go index 35920fef2cf..1c4a2b186bb 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client.go +++ b/pkg/plugins/backendplugin/grpcplugin/client.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/log" ) @@ -31,14 +30,13 @@ var handshake = goplugin.HandshakeConfig{ // pluginSet is list of plugins supported on v2. var pluginSet = map[int]goplugin.PluginSet{ grpcplugin.ProtocolVersion: { - "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, - "resource": &grpcplugin.ResourceGRPCPlugin{}, - "data": &grpcplugin.DataGRPCPlugin{}, - "stream": &grpcplugin.StreamGRPCPlugin{}, - "admission": &grpcplugin.AdmissionGRPCPlugin{}, - "conversion": &grpcplugin.ConversionGRPCPlugin{}, - "renderer": &pluginextensionv2.RendererGRPCPlugin{}, - "secretsmanager": &secretsmanagerplugin.SecretsManagerGRPCPlugin{}, + "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, + "resource": &grpcplugin.ResourceGRPCPlugin{}, + "data": &grpcplugin.DataGRPCPlugin{}, + "stream": &grpcplugin.StreamGRPCPlugin{}, + "admission": &grpcplugin.AdmissionGRPCPlugin{}, + "conversion": &grpcplugin.ConversionGRPCPlugin{}, + "renderer": &pluginextensionv2.RendererGRPCPlugin{}, }, } @@ -84,19 +82,15 @@ func newClientConfig(executablePath string, args []string, env []string, skipHos // StartRendererFunc callback function called when a renderer plugin is started. type StartRendererFunc func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error -// StartSecretsManagerFunc callback function called when a secrets manager plugin is started. -type StartSecretsManagerFunc func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error - // PluginDescriptor is a descriptor used for registering backend plugins. type PluginDescriptor struct { - pluginID string - executablePath string - executableArgs []string - skipHostEnvVars bool - managed bool - versionedPlugins map[int]goplugin.PluginSet - startRendererFn StartRendererFunc - startSecretsManagerFn StartSecretsManagerFunc + pluginID string + executablePath string + executableArgs []string + skipHostEnvVars bool + managed bool + versionedPlugins map[int]goplugin.PluginSet + startRendererFn StartRendererFunc } // NewBackendPlugin creates a new backend plugin factory used for registering a backend plugin. @@ -131,14 +125,3 @@ func NewRendererPlugin(pluginID, executablePath string, startFn StartRendererFun startRendererFn: startFn, }) } - -// NewSecretsManagerPlugin creates a new secrets manager plugin factory used for registering a backend secrets manager plugin. -func NewSecretsManagerPlugin(pluginID, executablePath string, startFn StartSecretsManagerFunc) backendplugin.PluginFactoryFunc { - return newPlugin(PluginDescriptor{ - pluginID: pluginID, - executablePath: executablePath, - managed: false, - versionedPlugins: pluginSet, - startSecretsManagerFn: startFn, - }) -} diff --git a/pkg/plugins/backendplugin/grpcplugin/client_v2.go b/pkg/plugins/backendplugin/grpcplugin/client_v2.go index ade867a2ace..78d7b0d6dcc 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_v2.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_v2.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/log" ) @@ -32,7 +31,6 @@ type ClientV2 struct { grpcplugin.AdmissionClient grpcplugin.ConversionClient pluginextensionv2.RendererPlugin - secretsmanagerplugin.SecretsManagerPlugin } func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugin.ClientProtocol) (*ClientV2, error) { @@ -71,11 +69,6 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi return nil, err } - rawSecretsManager, err := rpcClient.Dispense("secretsmanager") - if err != nil { - return nil, err - } - c := &ClientV2{} if rawDiagnostics != nil { if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok { @@ -119,24 +112,12 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi } } - if rawSecretsManager != nil { - if secretsManagerPlugin, ok := rawSecretsManager.(secretsmanagerplugin.SecretsManagerPlugin); ok { - c.SecretsManagerPlugin = secretsManagerPlugin - } - } - if descriptor.startRendererFn != nil { if err := descriptor.startRendererFn(descriptor.pluginID, c.RendererPlugin, logger); err != nil { return nil, err } } - if descriptor.startSecretsManagerFn != nil { - if err := descriptor.startSecretsManagerFn(descriptor.pluginID, c.SecretsManagerPlugin, logger); err != nil { - return nil, err - } - } - return c, nil } @@ -191,6 +172,14 @@ func (c *ClientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) return nil, plugins.ErrMethodNotImplemented } + if status.Code(err) == codes.Unavailable { + return nil, plugins.ErrPluginGrpcConnectionUnavailableBase.Errorf("%v", err) + } + + if status.Code(err) == codes.ResourceExhausted { + return nil, plugins.ErrPluginGrpcResourceExhaustedBase.Errorf("%v", err) + } + if errorSource, ok := backend.ErrorSourceFromGrpcStatusError(ctx, err); ok { return nil, handleGrpcStatusError(ctx, errorSource, err) } diff --git a/pkg/plugins/backendplugin/provider/provider.go b/pkg/plugins/backendplugin/provider/provider.go index 685c06a2df4..db9e3309a34 100644 --- a/pkg/plugins/backendplugin/provider/provider.go +++ b/pkg/plugins/backendplugin/provider/provider.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/log" ) @@ -21,7 +20,7 @@ type Service struct { func New(providers ...PluginBackendProvider) *Service { if len(providers) == 0 { - return New(SecretsManagerProvider, DefaultProvider) + return New(DefaultProvider) } return &Service{ providerChain: providers, @@ -29,7 +28,7 @@ func New(providers ...PluginBackendProvider) *Service { } func ProvideService(coreRegistry *coreplugin.Registry) *Service { - return New(coreRegistry.BackendFactoryProvider(), SecretsManagerProvider, DefaultProvider) + return New(coreRegistry.BackendFactoryProvider(), DefaultProvider) } func (s *Service) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { @@ -53,18 +52,6 @@ var RendererProvider PluginBackendProvider = func(_ context.Context, p *plugins. ) } -var SecretsManagerProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { - if !p.IsSecretsManager() { - return nil - } - return grpcplugin.NewSecretsManagerPlugin(p.ID, p.ExecutablePath(), - func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error { - p.SecretsManager = secretsmanager - return nil - }, - ) -} - var DefaultProvider = PluginBackendProvider(func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { return grpcplugin.NewBackendPlugin(p.ID, p.ExecutablePath(), p.SkipHostEnvVars) }) diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml b/pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml deleted file mode 100644 index 99beef65289..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: v1 -plugins: - - plugin: go - out: pkg/plugins/backendplugin/secretsmanagerplugin - opt: paths=source_relative - - plugin: go-grpc - out: pkg/plugins/backendplugin/secretsmanagerplugin - opt: - - paths=source_relative diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/buf.yaml b/pkg/plugins/backendplugin/secretsmanagerplugin/buf.yaml deleted file mode 100644 index 1a5194568a9..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/buf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -version: v1 -breaking: - use: - - FILE -lint: - use: - - DEFAULT diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go deleted file mode 100644 index bdecccee0d8..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go +++ /dev/null @@ -1,921 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.5 -// protoc (unknown) -// source: secretsmanager.proto - -package secretsmanagerplugin - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Key struct { - state protoimpl.MessageState `protogen:"open.v1"` - OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"` - Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` - Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Key) Reset() { - *x = Key{} - mi := &file_secretsmanager_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Key) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Key) ProtoMessage() {} - -func (x *Key) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Key.ProtoReflect.Descriptor instead. -func (*Key) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{0} -} - -func (x *Key) GetOrgId() int64 { - if x != nil { - return x.OrgId - } - return 0 -} - -func (x *Key) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *Key) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -type Item struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Item) Reset() { - *x = Item{} - mi := &file_secretsmanager_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Item) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Item) ProtoMessage() {} - -func (x *Item) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Item.ProtoReflect.Descriptor instead. -func (*Item) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{1} -} - -func (x *Item) GetKey() *Key { - if x != nil { - return x.Key - } - return nil -} - -func (x *Item) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type GetSecretRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSecretRequest) Reset() { - *x = GetSecretRequest{} - mi := &file_secretsmanager_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSecretRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSecretRequest) ProtoMessage() {} - -func (x *GetSecretRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSecretRequest.ProtoReflect.Descriptor instead. -func (*GetSecretRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{2} -} - -func (x *GetSecretRequest) GetKeyDescriptor() *Key { - if x != nil { - return x.KeyDescriptor - } - return nil -} - -type GetSecretResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"` - Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSecretResponse) Reset() { - *x = GetSecretResponse{} - mi := &file_secretsmanager_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSecretResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSecretResponse) ProtoMessage() {} - -func (x *GetSecretResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSecretResponse.ProtoReflect.Descriptor instead. -func (*GetSecretResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{3} -} - -func (x *GetSecretResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -func (x *GetSecretResponse) GetDecryptedValue() string { - if x != nil { - return x.DecryptedValue - } - return "" -} - -func (x *GetSecretResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -type SetSecretRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetSecretRequest) Reset() { - *x = SetSecretRequest{} - mi := &file_secretsmanager_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetSecretRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetSecretRequest) ProtoMessage() {} - -func (x *SetSecretRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SetSecretRequest.ProtoReflect.Descriptor instead. -func (*SetSecretRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{4} -} - -func (x *SetSecretRequest) GetKeyDescriptor() *Key { - if x != nil { - return x.KeyDescriptor - } - return nil -} - -func (x *SetSecretRequest) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type SetSecretResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetSecretResponse) Reset() { - *x = SetSecretResponse{} - mi := &file_secretsmanager_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetSecretResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetSecretResponse) ProtoMessage() {} - -func (x *SetSecretResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SetSecretResponse.ProtoReflect.Descriptor instead. -func (*SetSecretResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{5} -} - -func (x *SetSecretResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -type DeleteSecretRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSecretRequest) Reset() { - *x = DeleteSecretRequest{} - mi := &file_secretsmanager_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSecretRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSecretRequest) ProtoMessage() {} - -func (x *DeleteSecretRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteSecretRequest.ProtoReflect.Descriptor instead. -func (*DeleteSecretRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{6} -} - -func (x *DeleteSecretRequest) GetKeyDescriptor() *Key { - if x != nil { - return x.KeyDescriptor - } - return nil -} - -type DeleteSecretResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSecretResponse) Reset() { - *x = DeleteSecretResponse{} - mi := &file_secretsmanager_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSecretResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSecretResponse) ProtoMessage() {} - -func (x *DeleteSecretResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteSecretResponse.ProtoReflect.Descriptor instead. -func (*DeleteSecretResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{7} -} - -func (x *DeleteSecretResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -type ListSecretsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"` - AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSecretsRequest) Reset() { - *x = ListSecretsRequest{} - mi := &file_secretsmanager_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSecretsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSecretsRequest) ProtoMessage() {} - -func (x *ListSecretsRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSecretsRequest.ProtoReflect.Descriptor instead. -func (*ListSecretsRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{8} -} - -func (x *ListSecretsRequest) GetKeyDescriptor() *Key { - if x != nil { - return x.KeyDescriptor - } - return nil -} - -func (x *ListSecretsRequest) GetAllOrganizations() bool { - if x != nil { - return x.AllOrganizations - } - return false -} - -type ListSecretsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSecretsResponse) Reset() { - *x = ListSecretsResponse{} - mi := &file_secretsmanager_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSecretsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSecretsResponse) ProtoMessage() {} - -func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSecretsResponse.ProtoReflect.Descriptor instead. -func (*ListSecretsResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{9} -} - -func (x *ListSecretsResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -func (x *ListSecretsResponse) GetKeys() []*Key { - if x != nil { - return x.Keys - } - return nil -} - -type GetAllSecretsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAllSecretsRequest) Reset() { - *x = GetAllSecretsRequest{} - mi := &file_secretsmanager_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAllSecretsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAllSecretsRequest) ProtoMessage() {} - -func (x *GetAllSecretsRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetAllSecretsRequest.ProtoReflect.Descriptor instead. -func (*GetAllSecretsRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{10} -} - -type GetAllSecretsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - Items []*Item `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAllSecretsResponse) Reset() { - *x = GetAllSecretsResponse{} - mi := &file_secretsmanager_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAllSecretsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAllSecretsResponse) ProtoMessage() {} - -func (x *GetAllSecretsResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetAllSecretsResponse.ProtoReflect.Descriptor instead. -func (*GetAllSecretsResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{11} -} - -func (x *GetAllSecretsResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -func (x *GetAllSecretsResponse) GetItems() []*Item { - if x != nil { - return x.Items - } - return nil -} - -type RenameSecretRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"` - NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RenameSecretRequest) Reset() { - *x = RenameSecretRequest{} - mi := &file_secretsmanager_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RenameSecretRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RenameSecretRequest) ProtoMessage() {} - -func (x *RenameSecretRequest) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RenameSecretRequest.ProtoReflect.Descriptor instead. -func (*RenameSecretRequest) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{12} -} - -func (x *RenameSecretRequest) GetKeyDescriptor() *Key { - if x != nil { - return x.KeyDescriptor - } - return nil -} - -func (x *RenameSecretRequest) GetNewNamespace() string { - if x != nil { - return x.NewNamespace - } - return "" -} - -type RenameSecretResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RenameSecretResponse) Reset() { - *x = RenameSecretResponse{} - mi := &file_secretsmanager_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RenameSecretResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RenameSecretResponse) ProtoMessage() {} - -func (x *RenameSecretResponse) ProtoReflect() protoreflect.Message { - mi := &file_secretsmanager_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RenameSecretResponse.ProtoReflect.Descriptor instead. -func (*RenameSecretResponse) Descriptor() ([]byte, []int) { - return file_secretsmanager_proto_rawDescGZIP(), []int{13} -} - -func (x *RenameSecretResponse) GetUserFriendlyError() string { - if x != nil { - return x.UserFriendlyError - } - return "" -} - -var File_secretsmanager_proto protoreflect.FileDescriptor - -var file_secretsmanager_proto_rawDesc = string([]byte{ - 0x0a, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x4d, 0x0a, 0x03, - 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x49, 0x0a, 0x04, 0x49, - 0x74, 0x65, 0x6d, 0x12, 0x2b, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, - 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, - 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x01, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, - 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, - 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, - 0x26, 0x0a, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, - 0x69, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x41, 0x0a, 0x11, 0x53, 0x65, - 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, - 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x56, 0x0a, - 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x44, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, - 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, - 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x81, 0x01, 0x0a, 0x12, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, - 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, - 0x72, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, - 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, - 0x65, 0x79, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x77, 0x0a, 0x15, 0x47, - 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, - 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x30, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x22, 0x7a, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, - 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, - 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x22, 0x0a, 0x0c, - 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x22, 0x44, 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, - 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, - 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xe8, 0x04, 0x0a, 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x73, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x5c, 0x0a, 0x09, 0x47, 0x65, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, - 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, - 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0b, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x28, 0x2e, 0x73, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x65, 0x0a, 0x0c, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x12, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x6c, - 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x19, 0x5a, 0x17, 0x2e, 0x2f, 0x3b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -}) - -var ( - file_secretsmanager_proto_rawDescOnce sync.Once - file_secretsmanager_proto_rawDescData []byte -) - -func file_secretsmanager_proto_rawDescGZIP() []byte { - file_secretsmanager_proto_rawDescOnce.Do(func() { - file_secretsmanager_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_secretsmanager_proto_rawDesc), len(file_secretsmanager_proto_rawDesc))) - }) - return file_secretsmanager_proto_rawDescData -} - -var file_secretsmanager_proto_msgTypes = make([]protoimpl.MessageInfo, 14) -var file_secretsmanager_proto_goTypes = []any{ - (*Key)(nil), // 0: secretsmanagerplugin.Key - (*Item)(nil), // 1: secretsmanagerplugin.Item - (*GetSecretRequest)(nil), // 2: secretsmanagerplugin.GetSecretRequest - (*GetSecretResponse)(nil), // 3: secretsmanagerplugin.GetSecretResponse - (*SetSecretRequest)(nil), // 4: secretsmanagerplugin.SetSecretRequest - (*SetSecretResponse)(nil), // 5: secretsmanagerplugin.SetSecretResponse - (*DeleteSecretRequest)(nil), // 6: secretsmanagerplugin.DeleteSecretRequest - (*DeleteSecretResponse)(nil), // 7: secretsmanagerplugin.DeleteSecretResponse - (*ListSecretsRequest)(nil), // 8: secretsmanagerplugin.ListSecretsRequest - (*ListSecretsResponse)(nil), // 9: secretsmanagerplugin.ListSecretsResponse - (*GetAllSecretsRequest)(nil), // 10: secretsmanagerplugin.GetAllSecretsRequest - (*GetAllSecretsResponse)(nil), // 11: secretsmanagerplugin.GetAllSecretsResponse - (*RenameSecretRequest)(nil), // 12: secretsmanagerplugin.RenameSecretRequest - (*RenameSecretResponse)(nil), // 13: secretsmanagerplugin.RenameSecretResponse -} -var file_secretsmanager_proto_depIdxs = []int32{ - 0, // 0: secretsmanagerplugin.Item.key:type_name -> secretsmanagerplugin.Key - 0, // 1: secretsmanagerplugin.GetSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key - 0, // 2: secretsmanagerplugin.SetSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key - 0, // 3: secretsmanagerplugin.DeleteSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key - 0, // 4: secretsmanagerplugin.ListSecretsRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key - 0, // 5: secretsmanagerplugin.ListSecretsResponse.keys:type_name -> secretsmanagerplugin.Key - 1, // 6: secretsmanagerplugin.GetAllSecretsResponse.items:type_name -> secretsmanagerplugin.Item - 0, // 7: secretsmanagerplugin.RenameSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key - 2, // 8: secretsmanagerplugin.SecretsManager.GetSecret:input_type -> secretsmanagerplugin.GetSecretRequest - 4, // 9: secretsmanagerplugin.SecretsManager.SetSecret:input_type -> secretsmanagerplugin.SetSecretRequest - 6, // 10: secretsmanagerplugin.SecretsManager.DeleteSecret:input_type -> secretsmanagerplugin.DeleteSecretRequest - 8, // 11: secretsmanagerplugin.SecretsManager.ListSecrets:input_type -> secretsmanagerplugin.ListSecretsRequest - 12, // 12: secretsmanagerplugin.SecretsManager.RenameSecret:input_type -> secretsmanagerplugin.RenameSecretRequest - 10, // 13: secretsmanagerplugin.SecretsManager.GetAllSecrets:input_type -> secretsmanagerplugin.GetAllSecretsRequest - 3, // 14: secretsmanagerplugin.SecretsManager.GetSecret:output_type -> secretsmanagerplugin.GetSecretResponse - 5, // 15: secretsmanagerplugin.SecretsManager.SetSecret:output_type -> secretsmanagerplugin.SetSecretResponse - 7, // 16: secretsmanagerplugin.SecretsManager.DeleteSecret:output_type -> secretsmanagerplugin.DeleteSecretResponse - 9, // 17: secretsmanagerplugin.SecretsManager.ListSecrets:output_type -> secretsmanagerplugin.ListSecretsResponse - 13, // 18: secretsmanagerplugin.SecretsManager.RenameSecret:output_type -> secretsmanagerplugin.RenameSecretResponse - 11, // 19: secretsmanagerplugin.SecretsManager.GetAllSecrets:output_type -> secretsmanagerplugin.GetAllSecretsResponse - 14, // [14:20] is the sub-list for method output_type - 8, // [8:14] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name -} - -func init() { file_secretsmanager_proto_init() } -func file_secretsmanager_proto_init() { - if File_secretsmanager_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_secretsmanager_proto_rawDesc), len(file_secretsmanager_proto_rawDesc)), - NumEnums: 0, - NumMessages: 14, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_secretsmanager_proto_goTypes, - DependencyIndexes: file_secretsmanager_proto_depIdxs, - MessageInfos: file_secretsmanager_proto_msgTypes, - }.Build() - File_secretsmanager_proto = out.File - file_secretsmanager_proto_goTypes = nil - file_secretsmanager_proto_depIdxs = nil -} diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto deleted file mode 100644 index d967a05fa7b..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto +++ /dev/null @@ -1,78 +0,0 @@ -syntax = "proto3"; -package secretsmanagerplugin; - -option go_package = "./;secretsmanagerplugin"; - -message Key { - int64 orgId = 1; - string namespace = 2; - string type = 3; -} - -message Item { - Key key = 1; - string value = 2; -} - -message GetSecretRequest { - Key keyDescriptor = 1; -} - -message GetSecretResponse { - string userFriendlyError = 1; - string decryptedValue = 2; - bool exists = 3; -} - -message SetSecretRequest { - Key keyDescriptor = 1; - string value = 2; -} - -message SetSecretResponse { - string userFriendlyError = 1; -} - -message DeleteSecretRequest { - Key keyDescriptor = 1; -} - -message DeleteSecretResponse { - string userFriendlyError = 1; -} - -message ListSecretsRequest { - Key keyDescriptor = 1; - bool allOrganizations = 2; -} - -message ListSecretsResponse { - string userFriendlyError = 1; - repeated Key keys = 2; -} - -message GetAllSecretsRequest { -} - -message GetAllSecretsResponse { - string userFriendlyError = 1; - repeated Item items = 2; -} - -message RenameSecretRequest { - Key keyDescriptor = 1; - string newNamespace = 2; -} - -message RenameSecretResponse { - string userFriendlyError = 1; -} - -service SecretsManager { - rpc GetSecret(GetSecretRequest) returns (GetSecretResponse); - rpc SetSecret(SetSecretRequest) returns (SetSecretResponse); - rpc DeleteSecret(DeleteSecretRequest) returns (DeleteSecretResponse); - rpc ListSecrets(ListSecretsRequest) returns (ListSecretsResponse); - rpc RenameSecret(RenameSecretRequest) returns (RenameSecretResponse); - rpc GetAllSecrets(GetAllSecretsRequest) returns (GetAllSecretsResponse); -} diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go deleted file mode 100644 index ac65764e928..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go +++ /dev/null @@ -1,61 +0,0 @@ -package secretsmanagerplugin - -import ( - "context" - - "github.com/hashicorp/go-plugin" - "google.golang.org/grpc" -) - -type SecretsManagerPlugin interface { - SecretsManagerClient -} - -type SecretsManagerGRPCPlugin struct { - plugin.NetRPCUnsupportedPlugin -} - -func (p *SecretsManagerGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { - return nil -} - -func (p *SecretsManagerGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (any, error) { - return &SecretsManagerGRPCClient{NewSecretsManagerClient(c)}, nil -} - -type SecretsManagerGRPCClient struct { - SecretsManagerClient -} - -// Get an item from the store -func (sm *SecretsManagerGRPCClient) GetSecret(ctx context.Context, req *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) { - return sm.SecretsManagerClient.GetSecret(ctx, req) -} - -// Set an item in the store -func (sm *SecretsManagerGRPCClient) SetSecret(ctx context.Context, req *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) { - return sm.SecretsManagerClient.SetSecret(ctx, req) -} - -// Del deletes an item from the store. -func (sm *SecretsManagerGRPCClient) DeleteSecret(ctx context.Context, req *DeleteSecretRequest, opts ...grpc.CallOption) (*DeleteSecretResponse, error) { - return sm.SecretsManagerClient.DeleteSecret(ctx, req) -} - -// Keys get all keys for a given namespace. -func (sm *SecretsManagerGRPCClient) ListSecrets(ctx context.Context, req *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) { - return sm.SecretsManagerClient.ListSecrets(ctx, req) -} - -// Rename an item in the store -func (sm *SecretsManagerGRPCClient) RenameSecret(ctx context.Context, req *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) { - return sm.SecretsManagerClient.RenameSecret(ctx, req) -} - -// Get all items from the store -func (sm *SecretsManagerGRPCClient) GetAllSecrets(ctx context.Context, req *GetAllSecretsRequest, opts ...grpc.CallOption) (*GetAllSecretsResponse, error) { - return sm.SecretsManagerClient.GetAllSecrets(ctx, req) -} - -var _ SecretsManagerClient = &SecretsManagerGRPCClient{} -var _ plugin.GRPCPlugin = &SecretsManagerGRPCPlugin{} diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go deleted file mode 100644 index 3f23d1715fe..00000000000 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go +++ /dev/null @@ -1,300 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.4.0 -// - protoc (unknown) -// source: secretsmanager.proto - -package secretsmanagerplugin - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.62.0 or later. -const _ = grpc.SupportPackageIsVersion8 - -const ( - SecretsManager_GetSecret_FullMethodName = "/secretsmanagerplugin.SecretsManager/GetSecret" - SecretsManager_SetSecret_FullMethodName = "/secretsmanagerplugin.SecretsManager/SetSecret" - SecretsManager_DeleteSecret_FullMethodName = "/secretsmanagerplugin.SecretsManager/DeleteSecret" - SecretsManager_ListSecrets_FullMethodName = "/secretsmanagerplugin.SecretsManager/ListSecrets" - SecretsManager_RenameSecret_FullMethodName = "/secretsmanagerplugin.SecretsManager/RenameSecret" - SecretsManager_GetAllSecrets_FullMethodName = "/secretsmanagerplugin.SecretsManager/GetAllSecrets" -) - -// SecretsManagerClient is the client API for SecretsManager service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type SecretsManagerClient interface { - GetSecret(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) - SetSecret(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) - DeleteSecret(ctx context.Context, in *DeleteSecretRequest, opts ...grpc.CallOption) (*DeleteSecretResponse, error) - ListSecrets(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) - RenameSecret(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) - GetAllSecrets(ctx context.Context, in *GetAllSecretsRequest, opts ...grpc.CallOption) (*GetAllSecretsResponse, error) -} - -type secretsManagerClient struct { - cc grpc.ClientConnInterface -} - -func NewSecretsManagerClient(cc grpc.ClientConnInterface) SecretsManagerClient { - return &secretsManagerClient{cc} -} - -func (c *secretsManagerClient) GetSecret(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetSecretResponse) - err := c.cc.Invoke(ctx, SecretsManager_GetSecret_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *secretsManagerClient) SetSecret(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SetSecretResponse) - err := c.cc.Invoke(ctx, SecretsManager_SetSecret_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *secretsManagerClient) DeleteSecret(ctx context.Context, in *DeleteSecretRequest, opts ...grpc.CallOption) (*DeleteSecretResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(DeleteSecretResponse) - err := c.cc.Invoke(ctx, SecretsManager_DeleteSecret_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *secretsManagerClient) ListSecrets(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListSecretsResponse) - err := c.cc.Invoke(ctx, SecretsManager_ListSecrets_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *secretsManagerClient) RenameSecret(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(RenameSecretResponse) - err := c.cc.Invoke(ctx, SecretsManager_RenameSecret_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *secretsManagerClient) GetAllSecrets(ctx context.Context, in *GetAllSecretsRequest, opts ...grpc.CallOption) (*GetAllSecretsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetAllSecretsResponse) - err := c.cc.Invoke(ctx, SecretsManager_GetAllSecrets_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// SecretsManagerServer is the server API for SecretsManager service. -// All implementations must embed UnimplementedSecretsManagerServer -// for forward compatibility -type SecretsManagerServer interface { - GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) - SetSecret(context.Context, *SetSecretRequest) (*SetSecretResponse, error) - DeleteSecret(context.Context, *DeleteSecretRequest) (*DeleteSecretResponse, error) - ListSecrets(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error) - RenameSecret(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error) - GetAllSecrets(context.Context, *GetAllSecretsRequest) (*GetAllSecretsResponse, error) - mustEmbedUnimplementedSecretsManagerServer() -} - -// UnimplementedSecretsManagerServer must be embedded to have forward compatible implementations. -type UnimplementedSecretsManagerServer struct { -} - -func (UnimplementedSecretsManagerServer) GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetSecret not implemented") -} -func (UnimplementedSecretsManagerServer) SetSecret(context.Context, *SetSecretRequest) (*SetSecretResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetSecret not implemented") -} -func (UnimplementedSecretsManagerServer) DeleteSecret(context.Context, *DeleteSecretRequest) (*DeleteSecretResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteSecret not implemented") -} -func (UnimplementedSecretsManagerServer) ListSecrets(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListSecrets not implemented") -} -func (UnimplementedSecretsManagerServer) RenameSecret(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RenameSecret not implemented") -} -func (UnimplementedSecretsManagerServer) GetAllSecrets(context.Context, *GetAllSecretsRequest) (*GetAllSecretsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetAllSecrets not implemented") -} -func (UnimplementedSecretsManagerServer) mustEmbedUnimplementedSecretsManagerServer() {} - -// UnsafeSecretsManagerServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to SecretsManagerServer will -// result in compilation errors. -type UnsafeSecretsManagerServer interface { - mustEmbedUnimplementedSecretsManagerServer() -} - -func RegisterSecretsManagerServer(s grpc.ServiceRegistrar, srv SecretsManagerServer) { - s.RegisterService(&SecretsManager_ServiceDesc, srv) -} - -func _SecretsManager_GetSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetSecretRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).GetSecret(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_GetSecret_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).GetSecret(ctx, req.(*GetSecretRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SecretsManager_SetSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SetSecretRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).SetSecret(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_SetSecret_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).SetSecret(ctx, req.(*SetSecretRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SecretsManager_DeleteSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteSecretRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).DeleteSecret(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_DeleteSecret_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).DeleteSecret(ctx, req.(*DeleteSecretRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SecretsManager_ListSecrets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListSecretsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).ListSecrets(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_ListSecrets_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).ListSecrets(ctx, req.(*ListSecretsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SecretsManager_RenameSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RenameSecretRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).RenameSecret(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_RenameSecret_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).RenameSecret(ctx, req.(*RenameSecretRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SecretsManager_GetAllSecrets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetAllSecretsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SecretsManagerServer).GetAllSecrets(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SecretsManager_GetAllSecrets_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SecretsManagerServer).GetAllSecrets(ctx, req.(*GetAllSecretsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// SecretsManager_ServiceDesc is the grpc.ServiceDesc for SecretsManager service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var SecretsManager_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "secretsmanagerplugin.SecretsManager", - HandlerType: (*SecretsManagerServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "GetSecret", - Handler: _SecretsManager_GetSecret_Handler, - }, - { - MethodName: "SetSecret", - Handler: _SecretsManager_SetSecret_Handler, - }, - { - MethodName: "DeleteSecret", - Handler: _SecretsManager_DeleteSecret_Handler, - }, - { - MethodName: "ListSecrets", - Handler: _SecretsManager_ListSecrets_Handler, - }, - { - MethodName: "RenameSecret", - Handler: _SecretsManager_RenameSecret_Handler, - }, - { - MethodName: "GetAllSecrets", - Handler: _SecretsManager_GetAllSecrets_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "secretsmanager.proto", -} diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 63d87776c96..d020ab0c981 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -46,6 +46,7 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index 48caaa1a1c5..b6b4d943414 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -100,8 +100,8 @@ golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= diff --git a/pkg/plugins/errors.go b/pkg/plugins/errors.go index 1ab785ff7a3..77e35735ee0 100644 --- a/pkg/plugins/errors.go +++ b/pkg/plugins/errors.go @@ -35,4 +35,16 @@ var ( // Exposed as a base error to wrap it with plugin cancelled errors. ErrPluginRequestCanceledErrorBase = errutil.ClientClosedRequest("plugin.requestCanceled", errutil.WithPublicMessage("Plugin request canceled")) + + // ErrPluginGrpcResourceExhaustedBase error returned when a plugin response is larger than the grpc limit. + // Exposed as a base error to wrap it with plugin resource exhausted errors. + ErrPluginGrpcResourceExhaustedBase = errutil.Internal("plugin.resourceExhausted", + errutil.WithPublicMessage("The response is too large. Please try to reduce the time range or narrow down your query to return fewer data points."), + errutil.WithDownstream()) + + // ErrPluginGrpcConnectionUnavailableBase error returned when a plugin connection issue occurs. + // Exposed as a base error to wrap it with plugin connection issue errors. + ErrPluginGrpcConnectionUnavailableBase = errutil.Internal("plugin.connectionUnavailable", + errutil.WithPublicMessage("Data source became unavailable during request. Please try again."), + errutil.WithDownstream()) ) diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 368537db337..fd227184de5 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -98,11 +98,6 @@ type BackendFactoryProvider interface { BackendFactory(ctx context.Context, p *Plugin) backendplugin.PluginFactoryFunc } -type SecretsPluginManager interface { - // SecretsManager returns a secretsmanager plugin - SecretsManager(ctx context.Context) *Plugin -} - type StaticRouteResolver interface { Routes(ctx context.Context) []*StaticRoute } diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index 7ab5d6a60ff..4f4d19aa6b0 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -30,6 +30,14 @@ var ( errNilSender = errors.New("sender cannot be nil") ) +// passthroughErrors contains a list of errors that should be returned directly to the caller without wrapping +var passthroughErrors = []error{ + plugins.ErrPluginUnavailable, + plugins.ErrMethodNotImplemented, + plugins.ErrPluginGrpcResourceExhaustedBase, + plugins.ErrPluginGrpcConnectionUnavailableBase, +} + type Service struct { pluginRegistry registry.Service } @@ -52,12 +60,10 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) resp, err := p.QueryData(ctx, req) if err != nil { - if errors.Is(err, plugins.ErrMethodNotImplemented) { - return nil, err - } - - if errors.Is(err, plugins.ErrPluginUnavailable) { - return nil, err + for _, e := range passthroughErrors { + if errors.Is(err, e) { + return nil, err + } } if errors.Is(err, context.Canceled) { diff --git a/pkg/plugins/manager/client/client_test.go b/pkg/plugins/manager/client/client_test.go index f45761c3649..4964a3b1631 100644 --- a/pkg/plugins/manager/client/client_test.go +++ b/pkg/plugins/manager/client/client_test.go @@ -25,24 +25,39 @@ func TestQueryData(t *testing.T) { t.Run("Non-empty registry", func(t *testing.T) { tcs := []struct { - err error - expectedError error + err error + expectedError error + shouldPassThrough bool }{ { - err: plugins.ErrPluginUnavailable, - expectedError: plugins.ErrPluginUnavailable, + err: plugins.ErrPluginUnavailable, + expectedError: plugins.ErrPluginUnavailable, + shouldPassThrough: true, }, { - err: plugins.ErrMethodNotImplemented, - expectedError: plugins.ErrMethodNotImplemented, + err: plugins.ErrMethodNotImplemented, + expectedError: plugins.ErrMethodNotImplemented, + shouldPassThrough: true, }, { - err: errors.New("surprise surprise"), - expectedError: plugins.ErrPluginRequestFailureErrorBase, + err: errors.New("surprise surprise"), + expectedError: plugins.ErrPluginRequestFailureErrorBase, + shouldPassThrough: false, }, { - err: context.Canceled, - expectedError: plugins.ErrPluginRequestCanceledErrorBase, + err: context.Canceled, + expectedError: plugins.ErrPluginRequestCanceledErrorBase, + shouldPassThrough: false, + }, + { + err: plugins.ErrPluginGrpcConnectionUnavailableBase.Errorf("unavailable"), + expectedError: plugins.ErrPluginGrpcConnectionUnavailableBase.Errorf("unavailable"), + shouldPassThrough: true, + }, + { + err: plugins.ErrPluginGrpcResourceExhaustedBase.Errorf("exhausted"), + expectedError: plugins.ErrPluginGrpcResourceExhaustedBase.Errorf("exhausted"), + shouldPassThrough: true, }, } @@ -69,7 +84,11 @@ func TestQueryData(t *testing.T) { }, }) require.Error(t, err) - require.ErrorIs(t, err, tc.expectedError) + if tc.shouldPassThrough { + require.Equal(t, tc.err, err) + } else { + require.ErrorIs(t, err, tc.expectedError) + } }) } }) diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 80526b94dd5..23cd3be818e 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -90,6 +90,7 @@ func TestLoader_Load(t *testing.T) { }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, + {Name: "Documentation", URL: "https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/"}, }, }, Includes: []*plugins.Includes{ diff --git a/pkg/plugins/manager/pipeline/initialization/steps_test.go b/pkg/plugins/manager/pipeline/initialization/steps_test.go index da00444e93e..4892784b0ee 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps_test.go +++ b/pkg/plugins/manager/pipeline/initialization/steps_test.go @@ -64,30 +64,6 @@ func TestInitializer_Initialize(t *testing.T) { require.NotNil(t, c) }) - t.Run("secretsmanager", func(t *testing.T) { - p := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: "test", - Type: plugins.TypeSecretsManager, - Dependencies: plugins.Dependencies{ - GrafanaVersion: ">=8.x", - }, - Backend: true, - }, - Class: plugins.ClassExternal, - } - - stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}, fakes.InitializeNoopTracerForTest()) - - var err error - p, err = stepFunc(context.Background(), p) - require.NoError(t, err) - - c, exists := p.Client() - require.True(t, exists) - require.NotNil(t, c) - }) - t.Run("non backend plugin app", func(t *testing.T) { p := &plugins.Plugin{ JSONData: plugins.JSONData{ diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index f815d07c6b1..cdbde14df77 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" @@ -58,10 +57,9 @@ type Plugin struct { ExternalService *auth.ExternalService - Renderer pluginextensionv2.RendererPlugin - SecretsManager secretsmanagerplugin.SecretsManagerPlugin - client backendplugin.Plugin - log log.Logger + Renderer pluginextensionv2.RendererPlugin + client backendplugin.Plugin + log log.Logger SkipHostEnvVars bool @@ -126,7 +124,7 @@ type JSONData struct { SDK bool `json:"sdk,omitempty"` MultiValueFilterOperators bool `json:"multiValueFilterOperators,omitempty"` - // Backend (Datasource + Renderer + SecretsManager) + // Backend (Datasource + Renderer) Executable string `json:"executable,omitempty"` // App Service Auth Registration @@ -442,10 +440,6 @@ func (p *Plugin) ExecutablePath() string { return p.executablePath("plugin_start") } - if p.IsSecretsManager() { - return p.executablePath("secrets_plugin_start") - } - return p.executablePath(p.Executable) } @@ -486,10 +480,6 @@ func (p *Plugin) IsRenderer() bool { return p.Type == TypeRenderer } -func (p *Plugin) IsSecretsManager() bool { - return p.Type == TypeSecretsManager -} - func (p *Plugin) IsApp() bool { return p.Type == TypeApp } @@ -519,22 +509,20 @@ var PluginTypes = []Type{ TypePanel, TypeApp, TypeRenderer, - TypeSecretsManager, } type Type string const ( - TypeDataSource Type = "datasource" - TypePanel Type = "panel" - TypeApp Type = "app" - TypeRenderer Type = "renderer" - TypeSecretsManager Type = "secretsmanager" + TypeDataSource Type = "datasource" + TypePanel Type = "panel" + TypeApp Type = "app" + TypeRenderer Type = "renderer" ) func (pt Type) IsValid() bool { switch pt { - case TypeDataSource, TypePanel, TypeApp, TypeRenderer, TypeSecretsManager: + case TypeDataSource, TypePanel, TypeApp, TypeRenderer: return true } return false diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 4b4393c9c70..859cbe020a1 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,7 +4,7 @@ go 1.23.7 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.272.0 + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.21.0 github.com/prometheus/common v0.62.0 @@ -18,9 +18,10 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -33,7 +34,7 @@ require ( github.com/dennwc/varint v1.0.0 // indirect github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -41,10 +42,11 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -62,8 +64,8 @@ require ( github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect @@ -79,7 +81,7 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -99,7 +101,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -111,14 +113,14 @@ require ( golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.220.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 899f8550654..473cb58301f 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ= cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= @@ -13,20 +14,21 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkY github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= @@ -60,8 +62,8 @@ github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVo github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -85,18 +87,18 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -121,8 +123,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -162,10 +164,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -219,8 +221,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -301,8 +303,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= @@ -364,8 +366,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= @@ -389,12 +391,12 @@ gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index aebcfcbbdd0..0ba63af8484 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/userstorage" ) @@ -27,6 +28,7 @@ func ProvideRegistryServiceSink( _ *query.QueryAPIBuilder, _ *notifications.NotificationsAPIBuilder, _ *userstorage.UserStorageAPIBuilder, + _ *secret.SecretAPIBuilder, _ *provisioning.APIBuilder, ) *Service { return &Service{} diff --git a/pkg/registry/apis/dashboard/authorizer.go b/pkg/registry/apis/dashboard/authorizer.go deleted file mode 100644 index 0b51cd32c68..00000000000 --- a/pkg/registry/apis/dashboard/authorizer.go +++ /dev/null @@ -1,93 +0,0 @@ -package dashboard - -import ( - "context" - - "k8s.io/apiserver/pkg/authorization/authorizer" - - claims "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/guardian" -) - -func GetAuthorizer(dashboardService dashboards.DashboardService, l log.Logger) authorizer.Authorizer { - return authorizer.AuthorizerFunc( - func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - // Use the standard authorizer - if !attr.IsResourceRequest() { - return authorizer.DecisionNoOpinion, "", nil - } - - user, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "", err - } - - // Allow search and list requests - if attr.GetResource() == "search" || attr.GetName() == "" { - return authorizer.DecisionNoOpinion, "", nil - } - - ns := attr.GetNamespace() - if ns == "" { - return authorizer.DecisionDeny, "expected namespace", nil - } - - info, err := claims.ParseNamespace(attr.GetNamespace()) - if err != nil { - return authorizer.DecisionDeny, "error reading org from namespace", err - } - - // expensive path to lookup permissions for a single dashboard - // must include deleted to allow for restores - dto, err := dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{ - UID: attr.GetName(), - OrgID: info.OrgID, - IncludeDeleted: true, - }) - if err != nil { - return authorizer.DecisionDeny, "error loading dashboard", err - } - - ok := false - guardian, err := guardian.NewByDashboard(ctx, dto, info.OrgID, user) - if err != nil { - return authorizer.DecisionDeny, "", err - } - - switch attr.GetVerb() { - case "get": - ok, err = guardian.CanView() - if !ok || err != nil { - return authorizer.DecisionDeny, "can not view dashboard", err - } - case "create": - fallthrough - case "post": - ok, err = guardian.CanSave() // vs Edit? - if !ok || err != nil { - return authorizer.DecisionDeny, "can not save dashboard", err - } - case "update": - fallthrough - case "patch": - fallthrough - case "put": - ok, err = guardian.CanEdit() // vs Save - if !ok || err != nil { - return authorizer.DecisionDeny, "can not edit dashboard", err - } - case "delete": - ok, err = guardian.CanDelete() - if !ok || err != nil { - return authorizer.DecisionDeny, "can not delete dashboard", err - } - default: - l.Info("unknown verb", "verb", attr.GetVerb()) - return authorizer.DecisionNoOpinion, "unsupported verb", nil // Unknown verb - } - return authorizer.DecisionAllow, "", nil - }) -} diff --git a/pkg/registry/apis/dashboard/latest.go b/pkg/registry/apis/dashboard/latest.go deleted file mode 100644 index 0944bcdf537..00000000000 --- a/pkg/registry/apis/dashboard/latest.go +++ /dev/null @@ -1,103 +0,0 @@ -package dashboard - -import ( - "context" - "fmt" - "net/http" - "strconv" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" - - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/storage/unified/resource" -) - -// LatestConnector will return the latest version of the resource - even if it is deleted -type LatestConnector interface { - rest.Storage - rest.Connecter - rest.StorageMetadata -} - -func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource) LatestConnector { - return &latestREST{ - unified: unified, - gr: gr, - } -} - -type latestREST struct { - unified resource.ResourceClient - gr schema.GroupResource -} - -func (l *latestREST) New() runtime.Object { - return &metav1.PartialObjectMetadataList{} -} - -func (l *latestREST) Destroy() { -} - -func (l *latestREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (l *latestREST) ProducesMIMETypes(verb string) []string { - return nil -} - -func (l *latestREST) ProducesObject(verb string) interface{} { - return &metav1.PartialObjectMetadataList{} -} - -func (l *latestREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" -} - -func (l *latestREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - key := &resource.ResourceKey{ - Namespace: info.Value, - Group: l.gr.Group, - Resource: l.gr.Resource, - Name: uid, - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - rsp, err := l.unified.Read(ctx, &resource.ReadRequest{ - Key: key, - ResourceVersion: 0, // 0 will return the latest version that was not a delete event - IncludeDeleted: true, - }) - if err != nil { - responder.Error(err) - return - } else if rsp == nil || (rsp.Error != nil && rsp.Error.Code == http.StatusNotFound) { - responder.Error(storage.NewKeyNotFoundError(uid, 0)) - return - } else if rsp.Error != nil { - responder.Error(fmt.Errorf("could not retrieve object: %s", rsp.Error.Message)) - return - } - - uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, rsp.Value) - if err != nil { - responder.Error(fmt.Errorf("could not convert object: %s", err.Error())) - return - } - - finalObj := uncastObj.(*unstructured.Unstructured) - finalObj.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10)) - - responder.Object(http.StatusOK, finalObj) - }), nil -} diff --git a/pkg/registry/apis/dashboard/latest_test.go b/pkg/registry/apis/dashboard/latest_test.go deleted file mode 100644 index cdf54985a70..00000000000 --- a/pkg/registry/apis/dashboard/latest_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package dashboard - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "reflect" - "strconv" - "testing" - - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/request" -) - -func TestLatest(t *testing.T) { - gr := schema.GroupResource{ - Group: "group", - Resource: "resource", - } - ctx := context.Background() - mockResponder := &mockResponder{} - mockClient := &mockResourceClient{} - r := &latestREST{ - unified: mockClient, - gr: gr, - } - - t.Run("no namespace in context", func(t *testing.T) { - _, err := r.Connect(ctx, "test-uid", nil, mockResponder) - require.Error(t, err) - }) - - ctx = request.WithNamespace(context.Background(), "default") - - t.Run("happy path", func(t *testing.T) { - req := httptest.NewRequest("GET", "/latest", nil) - w := httptest.NewRecorder() - - readReq := &resource.ReadRequest{ - Key: &resource.ResourceKey{ - Namespace: "default", - Group: "group", - Resource: "resource", - Name: "uid", - }, - ResourceVersion: 0, - IncludeDeleted: true, - } - - expectedObject := &metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "resource", - APIVersion: "v0alpha1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "uid", - Namespace: "default", - ResourceVersion: strconv.FormatInt(123, 10), - }, - } - - val, err := json.Marshal(expectedObject) - require.NoError(t, err) - mockClient.On("Read", ctx, readReq).Return(&resource.ReadResponse{ - ResourceVersion: 123, - Value: val, - }, nil).Once() - - mockResponder.On("Object", http.StatusOK, mock.MatchedBy(func(obj interface{}) bool { - unstructuredObj, ok := obj.(*unstructured.Unstructured) - expectedMap := map[string]interface{}{ - "apiVersion": expectedObject.APIVersion, - "kind": expectedObject.Kind, - "metadata": map[string]interface{}{ - "name": expectedObject.Name, - "namespace": expectedObject.Namespace, - "resourceVersion": expectedObject.ResourceVersion, - "creationTimestamp": nil, - }, - } - return ok && reflect.DeepEqual(unstructuredObj.Object, expectedMap) - })) - - handler, err := r.Connect(ctx, "uid", nil, mockResponder) - require.NoError(t, err) - handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) - - mockClient.AssertExpectations(t) - mockResponder.AssertExpectations(t) - }) -} diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index afcd1e0fddf..d85c66047c9 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -70,11 +70,6 @@ func (d *directResourceClient) Read(ctx context.Context, in *resource.ReadReques return d.server.Read(ctx, in) } -// Restore implements ResourceClient. -func (d *directResourceClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { - return d.server.Restore(ctx, in) -} - // Search implements ResourceClient. func (d *directResourceClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { return d.server.Search(ctx, in) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 8f07f37658b..bc9b5a2c4e0 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -404,6 +404,7 @@ func (a *dashboardSqlAccess) buildSaveDashboardCommand(ctx context.Context, orgI }) if old != nil { dash.Spec.Set("id", old.ID) + dash.Spec.Set("version", float64(old.Version)) } else { dash.Spec.Remove("id") // existing of "id" makes it an update created = true diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index 2e8d5485e9e..efa4ffde96f 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -169,6 +169,7 @@ func TestBuildSaveDashboardCommand(t *testing.T) { mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return( &dashboards.Dashboard{ ID: 1234, + Version: 2, APIVersion: "dashboard.grafana.app/v0alpha1", }, nil).Once() cmd, created, err = access.buildSaveDashboardCommand(ctx, 1, dash) @@ -176,8 +177,9 @@ func TestBuildSaveDashboardCommand(t *testing.T) { require.Equal(t, false, created) require.NotNil(t, cmd) require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString()) - require.Equal(t, cmd.Dashboard.Get("id").MustInt64(), int64(1234)) // should set to existing ID - require.Equal(t, cmd.APIVersion, "v0alpha1") // should trim prefix + require.Equal(t, cmd.Dashboard.Get("id").MustInt64(), int64(1234)) // should set to existing ID + require.Equal(t, cmd.Dashboard.Get("version").MustFloat64(), float64(2)) // version must be set - otherwise seen as a new dashboard in NewDashboardFromJson + require.Equal(t, cmd.APIVersion, "v0alpha1") // should trim prefix require.Equal(t, cmd.OrgID, int64(1)) require.True(t, cmd.Overwrite) } diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 20445e5aa6c..2037376723f 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -39,6 +39,10 @@ func (s *DashboardStorage) NewStore(dash utils.ResourceInfo, scheme *runtime.Sch optsGetter := apistore.NewRESTOptionsGetterForClient(client, defaultOpts.StorageConfig.Config, ) + optsGetter.RegisterOptions(dash.GroupResource(), apistore.StorageOptions{ + EnableFolderSupport: true, + RequireDeprecatedInternalID: true, + }) store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter) return &storeWrapper{ diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index fd5e2283b27..be67dcddb9c 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -34,11 +34,33 @@ func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Servi } var sortByMapping = map[string]string{ - unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently-", - unisearch.DASHBOARD_VIEWS_TOTAL: "viewed-", - unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently-", - unisearch.DASHBOARD_ERRORS_TOTAL: "errors-", - "title": "alpha-", + unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently", + unisearch.DASHBOARD_VIEWS_TOTAL: "viewed", + unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently", + unisearch.DASHBOARD_ERRORS_TOTAL: "errors", + "title": "alpha", +} + +func ParseSortName(sortName string) (string, bool, error) { + if sortName == "" { + return "", false, nil + } + + isDesc := strings.HasSuffix(sortName, "-desc") + isAsc := strings.HasSuffix(sortName, "-asc") + // default to desc if no suffix is provided + if !isDesc && !isAsc { + isDesc = true + } + + prefix := strings.TrimSuffix(strings.TrimSuffix(sortName, "-desc"), "-asc") + for key, mappedPrefix := range sortByMapping { + if prefix == mappedPrefix { + return key, isDesc, nil + } + } + + return "", false, fmt.Errorf("no matching sort field found for: %s", sortName) } // nolint:gocyclo @@ -99,9 +121,9 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour sorterName := sortByMapping[sortByField] if sort.Desc { - sorterName += "desc" + sorterName += "-desc" } else { - sorterName += "asc" + sorterName += "-asc" } if sorter, ok := c.sorter.GetSortOption(sorterName); ok { @@ -207,22 +229,27 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour } } searchFields := resource.StandardSearchFields() + columns := []*resource.ResourceTableColumnDefinition{ + searchFields.Field(resource.SEARCH_FIELD_TITLE), + searchFields.Field(resource.SEARCH_FIELD_FOLDER), + searchFields.Field(resource.SEARCH_FIELD_TAGS), + { + Name: unisearch.DASHBOARD_LEGACY_ID, + Type: resource.ResourceTableColumnDefinition_INT64, + Description: "Deprecated legacy id of the dashboard", + }, + } + + if sortByField != "" { + columns = append(columns, &resource.ResourceTableColumnDefinition{ + Name: sortByField, + Type: resource.ResourceTableColumnDefinition_INT64, + }) + } + list := &resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - searchFields.Field(resource.SEARCH_FIELD_TITLE), - searchFields.Field(resource.SEARCH_FIELD_FOLDER), - searchFields.Field(resource.SEARCH_FIELD_TAGS), - { - Name: unisearch.DASHBOARD_LEGACY_ID, - Type: resource.ResourceTableColumnDefinition_INT64, - Description: "Deprecated legacy id of the dashboard", - }, - { - Name: sortByField, - Type: resource.ResourceTableColumnDefinition_INT64, - }, - }, + Columns: columns, }, } @@ -249,11 +276,22 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour } for _, dashboard := range dashes { + cells := [][]byte{ + []byte(dashboard.Title), + []byte(dashboard.FolderUID), + []byte("[]"), // no tags retrieved for provisioned dashboards + []byte(strconv.FormatInt(dashboard.ID, 10)), + } + + if sortByField != "" { + cells = append(cells, []byte("0")) + } + list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{ Key: getResourceKey(&dashboards.DashboardSearchProjection{ UID: dashboard.UID, }, req.Options.Key.Namespace), - Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), []byte(strconv.FormatInt(dashboard.ID, 10)), {}, {}}, + Cells: cells, }) } @@ -275,9 +313,20 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour return nil, err } + cells := [][]byte{ + []byte(dashboard.Title), + []byte(dashboard.FolderUID), + tags, + []byte(strconv.FormatInt(dashboard.ID, 10)), + } + + if sortByField != "" { + cells = append(cells, []byte(strconv.FormatInt(dashboard.SortMeta, 10))) + } + list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{ Key: getResourceKey(dashboard, req.Options.Key.Namespace), - Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags, []byte(strconv.FormatInt(dashboard.ID, 10)), []byte(strconv.FormatInt(dashboard.SortMeta, 10))}, + Cells: cells, }) } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index a4eecae9058..700fad7a2ce 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -77,10 +77,6 @@ func TestDashboardSearchClient_Search(t *testing.T) { Type: resource.ResourceTableColumnDefinition_INT64, Description: "Deprecated legacy id of the dashboard", }, - { - Name: "", // sort by should be empty if title is what we sorted by - Type: resource.ResourceTableColumnDefinition_INT64, - }, }, Rows: []*resource.ResourceTableRow{ { @@ -94,7 +90,6 @@ func TestDashboardSearchClient_Search(t *testing.T) { []byte("folder1"), tags, []byte("1"), - []byte(strconv.FormatInt(0, 10)), }, }, { @@ -108,7 +103,6 @@ func TestDashboardSearchClient_Search(t *testing.T) { []byte("folder2"), emptyTags, []byte("2"), - []byte(strconv.FormatInt(0, 10)), }, }, }, @@ -501,4 +495,131 @@ func TestDashboardSearchClient_Search(t *testing.T) { } require.Equal(t, resp.TotalHits, int64(1)) }) + + t.Run("Should set empty sort field when sorting by title", func(t *testing.T) { + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Sort: sort.SortAlphaAsc, + Type: "dash-db", + }).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + + req := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: dashboardKey, + }, + SortBy: []*resource.ResourceSearchRequest_Sort{ + { + Field: resource.SEARCH_FIELD_TITLE, + }, + }, + } + resp, err := client.Search(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, resp.Results.Columns, 4) + mockStore.AssertExpectations(t) + }) + + t.Run("Should set correct sort field when sorting by views", func(t *testing.T) { + mockStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", SortMeta: 100}, + }, nil).Once() + + req := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: dashboardKey, + }, + SortBy: []*resource.ResourceSearchRequest_Sort{ + { + Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, + }, + }, + } + resp, err := client.Search(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + require.Len(t, resp.Results.Columns, 5) + i := len(resp.Results.Columns) - 1 + require.Equal(t, "views_total", resp.Results.Columns[i].Name) + require.Equal(t, []byte(strconv.FormatInt(100, 10)), resp.Results.Rows[0].Cells[i]) // views should be set to 100 + mockStore.AssertExpectations(t) + }) +} + +func TestParseSortName(t *testing.T) { + tests := []struct { + name string + sortName string + wantField string + wantDesc bool + wantErr bool + }{ + { + name: "empty sort name", + sortName: "", + wantField: "", + wantDesc: false, + wantErr: false, + }, + { + name: "viewed-recently with desc suffix", + sortName: "viewed-recently-desc", + wantField: unisearch.DASHBOARD_VIEWS_LAST_30_DAYS, + wantDesc: true, + wantErr: false, + }, + { + name: "defaults to desc", + sortName: "viewed", + wantField: unisearch.DASHBOARD_VIEWS_TOTAL, + wantDesc: true, + wantErr: false, + }, + { + name: "errors-recentlyy with asc suffix", + sortName: "errors-recently-asc", + wantField: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS, + wantDesc: false, + wantErr: false, + }, + { + name: "errors - defaults to desc too", + sortName: "errors", + wantField: unisearch.DASHBOARD_ERRORS_TOTAL, + wantDesc: true, + wantErr: false, + }, + { + name: "alpha sort with asc suffix", + sortName: "alpha-asc", + wantField: "title", + wantDesc: false, + wantErr: false, + }, + { + name: "invalid sort name", + sortName: "invalid-sort-desc", + wantField: "", + wantDesc: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field, isDesc, err := ParseSortName(tt.sortName) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantField, field) + require.Equal(t, tt.wantDesc, isDesc) + }) + } } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index d09af5df91c..c8a7b6f9f55 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -184,6 +184,7 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storageOpts := apistore.StorageOptions{ + EnableFolderSupport: true, RequireDeprecatedInternalID: true, } @@ -280,11 +281,6 @@ func (b *DashboardsAPIBuilder) storageForVersion( return err } - if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) { - storage[dashboards.StoragePath("restore")] = NewRestoreConnector(b.unified, gr) - storage[dashboards.StoragePath("latest")] = NewLatestConnector(b.unified, gr) - } - // Register the DTO endpoint that will consolidate all dashboard bits storage[dashboards.StoragePath("dto")], err = NewDTOConnector( storage[dashboards.StoragePath()].(rest.Getter), diff --git a/pkg/registry/apis/dashboard/restore.go b/pkg/registry/apis/dashboard/restore.go deleted file mode 100644 index 839516cbb31..00000000000 --- a/pkg/registry/apis/dashboard/restore.go +++ /dev/null @@ -1,122 +0,0 @@ -package dashboard - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" - - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/storage/unified/resource" -) - -type RestoreConnector interface { - rest.Storage - rest.Connecter - rest.StorageMetadata -} - -func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource) RestoreConnector { - return &restoreREST{ - unified: unified, - gr: gr, - } -} - -type restoreREST struct { - unified resource.ResourceClient - gr schema.GroupResource -} - -func (r *restoreREST) New() runtime.Object { - return &metav1.PartialObjectMetadataList{} -} - -func (r *restoreREST) Destroy() { -} - -func (r *restoreREST) ConnectMethods() []string { - return []string{"POST"} -} - -func (r *restoreREST) ProducesMIMETypes(verb string) []string { - return nil -} - -func (r *restoreREST) ProducesObject(verb string) interface{} { - return &metav1.PartialObjectMetadataList{} -} - -func (r *restoreREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" -} - -type RestoreOptions struct { - ResourceVersion int64 `json:"resourceVersion"` -} - -func (r *restoreREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - key := &resource.ResourceKey{ - Namespace: info.Value, - Group: r.gr.Group, - Resource: r.gr.Resource, - Name: uid, - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) - if err != nil { - responder.Error(fmt.Errorf("unable to read request body: %s", err.Error())) - return - } - reqBody := &RestoreOptions{} - err = json.Unmarshal(body, &reqBody) - if err != nil { - responder.Error(fmt.Errorf("unable to unmarshal request body: %s", err.Error())) - return - } - - if reqBody.ResourceVersion == 0 { - responder.Error(fmt.Errorf("resource version required")) - return - } - - rsp, err := r.unified.Restore(ctx, &resource.RestoreRequest{ - ResourceVersion: reqBody.ResourceVersion, - Key: key, - }) - if err != nil { - responder.Error(err) - return - } else if rsp == nil || (rsp.Error != nil && rsp.Error.Code == http.StatusNotFound) { - responder.Error(storage.NewKeyNotFoundError(uid, reqBody.ResourceVersion)) - return - } else if rsp.Error != nil { - responder.Error(fmt.Errorf("could not re-create object: %s", rsp.Error.Message)) - return - } - - obj := metav1.PartialObjectMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: key.Name, - Namespace: key.Namespace, - ResourceVersion: strconv.FormatInt(rsp.ResourceVersion, 10), - }, - } - - responder.Object(http.StatusOK, &obj) - }), nil -} diff --git a/pkg/registry/apis/dashboard/restore_test.go b/pkg/registry/apis/dashboard/restore_test.go deleted file mode 100644 index 26edde534ef..00000000000 --- a/pkg/registry/apis/dashboard/restore_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package dashboard - -import ( - "bytes" - "context" - "fmt" - "net/http" - "net/http/httptest" - "strconv" - "testing" - - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/request" -) - -type mockResourceClient struct { - mock.Mock - resource.ResourceClient -} - -func (m *mockResourceClient) Restore(ctx context.Context, req *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(*resource.RestoreResponse), args.Error(1) -} - -func (m *mockResourceClient) Read(ctx context.Context, req *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(*resource.ReadResponse), args.Error(1) -} - -type mockResponder struct { - mock.Mock -} - -func (m *mockResponder) Object(statusCode int, obj runtime.Object) { - m.Called(statusCode, obj) -} - -func (m *mockResponder) Error(err error) { - m.Called(err) -} - -func TestRestore(t *testing.T) { - gr := schema.GroupResource{ - Group: "group", - Resource: "resource", - } - ctx := context.Background() - mockResponder := &mockResponder{} - mockClient := &mockResourceClient{} - r := &restoreREST{ - unified: mockClient, - gr: gr, - } - - t.Run("no namespace in context", func(t *testing.T) { - _, err := r.Connect(ctx, "test-uid", nil, mockResponder) - assert.Error(t, err) - }) - - ctx = request.WithNamespace(context.Background(), "default") - - t.Run("invalid resourceVersion", func(t *testing.T) { - req := httptest.NewRequest("POST", "/restore", bytes.NewReader([]byte(`{"resourceVersion":0}`))) - w := httptest.NewRecorder() - - expectedError := fmt.Errorf("resource version required") - mockResponder.On("Error", mock.MatchedBy(func(err error) bool { - return err.Error() == expectedError.Error() - })) - - handler, err := r.Connect(ctx, "test-uid", nil, mockResponder) - assert.NoError(t, err) - - handler.ServeHTTP(w, req) - mockResponder.AssertExpectations(t) - }) - - t.Run("happy path", func(t *testing.T) { - req := httptest.NewRequest("POST", "/restore", bytes.NewReader([]byte(`{"resourceVersion":123}`))) - w := httptest.NewRecorder() - restoreReq := &resource.RestoreRequest{ - ResourceVersion: 123, - Key: &resource.ResourceKey{ - Namespace: "default", - Group: "group", - Resource: "resource", - Name: "uid", - }, - } - - expectedObject := &metav1.PartialObjectMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: "uid", - Namespace: "default", - ResourceVersion: strconv.FormatInt(123, 10), - }, - } - - mockClient.On("Restore", ctx, restoreReq).Return(&resource.RestoreResponse{ - ResourceVersion: 123, - }, nil).Once() - - mockResponder.On("Object", http.StatusOK, mock.MatchedBy(func(obj interface{}) bool { - metadata, ok := obj.(*metav1.PartialObjectMetadata) - return ok && - metadata.ObjectMeta.Name == "uid" && - metadata.ObjectMeta.Namespace == "default" && - metadata.ObjectMeta.ResourceVersion == "123" - })).Return(expectedObject) - - handler, err := r.Connect(ctx, "uid", nil, mockResponder) - assert.NoError(t, err) - handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) - - mockClient.AssertExpectations(t) - mockResponder.AssertExpectations(t) - }) -} diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 33162ccabb6..86967a9f5d3 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -407,6 +407,7 @@ func asResourceKey(ns string, k string) (*resource.ResourceKey, error) { func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, user identity.Requester) ([]string, error) { if !s.features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) { + s.log.Warn("Tried to search for 'sharedwithme' dashboards with ", featuremgmt.FlagUnifiedStorageSearchPermissionFiltering, " disabled") return []string{}, nil } @@ -459,7 +460,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } if folderUidIdx == -1 { - return sharedDashboards, fmt.Errorf("Error retrieving folder information") + return sharedDashboards, fmt.Errorf("error retrieving folder information") } // populate list of unique folder UIDs in the list of dashboards user has read permissions diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index e393aabea62..90411b3cbbf 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -692,9 +692,6 @@ func (m *MockClient) Update(ctx context.Context, in *resource.UpdateRequest, opt func (m *MockClient) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { return nil, nil } -func (m *MockClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { - return nil, nil -} func (m *MockClient) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) { return nil, nil } diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index 5768ce906d5..32f0b787ee8 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -87,7 +86,7 @@ func (r *DTOConnector) ProducesObject(verb string) interface{} { } func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) + _, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err } @@ -128,33 +127,22 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob return } - // Calculate access information -- needed to help smooth transition from /api/dashboard format - dto := &dashboards.Dashboard{ - UID: name, - OrgID: info.OrgID, - ID: obj.GetDeprecatedInternalID(), // nolint:staticcheck - } - manager, ok := obj.GetManagerProperties() - if ok && manager.Kind == utils.ManagerKindPlugin { - dto.PluginID = manager.Identity - } - - guardian, err := guardian.NewByDashboard(ctx, dto, info.OrgID, user) - if err != nil { - responder.Error(err) - return - } - canView, err := guardian.CanView() + dashScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(name) + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashScope) + canView, err := r.accessControl.Evaluate(ctx, user, evaluator) if err != nil || !canView { responder.Error(fmt.Errorf("not allowed to view")) return } access := &dashboard.DashboardAccess{} - access.CanEdit, _ = guardian.CanEdit() - access.CanSave, _ = guardian.CanSave() - access.CanAdmin, _ = guardian.CanAdmin() - access.CanDelete, _ = guardian.CanDelete() + writeEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashScope) + access.CanSave, _ = r.accessControl.Evaluate(ctx, user, writeEvaluator) + access.CanEdit = access.CanSave + adminEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashScope) + access.CanAdmin, _ = r.accessControl.Evaluate(ctx, user, adminEvaluator) + deleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashScope) + access.CanDelete, _ = r.accessControl.Evaluate(ctx, user, deleteEvaluator) access.CanStar = user.IsIdentityType(claims.TypeUser) access.AnnotationsPermissions = &dashboard.AnnotationPermission{} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index ca3d492815d..df8b2431f2f 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - authtypes "github.com/grafana/authlib/types" "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -18,6 +17,7 @@ import ( common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + authtypes "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" @@ -156,6 +156,7 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API } opts.StorageOptions(resourceInfo.GroupResource(), apistore.StorageOptions{ + EnableFolderSupport: true, RequireDeprecatedInternalID: true}) folderStore := &folderStorage{ diff --git a/pkg/registry/apis/provisioning/apifmt/error.go b/pkg/registry/apis/provisioning/apifmt/error.go new file mode 100644 index 00000000000..c5d4b4669d6 --- /dev/null +++ b/pkg/registry/apis/provisioning/apifmt/error.go @@ -0,0 +1,109 @@ +// apifmt aims to provide a Kubernetes-compatible way to format text. +package apifmt + +import ( + "errors" + "fmt" + "net/http" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + _ error = (*fmtError)(nil) + _ apierrors.APIStatus = (*fmtError)(nil) +) + +type fmtError struct { + inner error + str string + + innerStatusErr apierrors.APIStatus + initInnerStatusErr bool +} + +func (e *fmtError) Error() string { + return e.str +} + +// Status returns the status that is closest in the tree, in a depth-first search. +func (e *fmtError) Status() metav1.Status { + if !e.initInnerStatusErr { + if status, ok := e.inner.(apierrors.APIStatus); ok || errors.As(e.inner, &status) { + e.innerStatusErr = status + } + e.initInnerStatusErr = true + } + + status := metav1.Status{ + Message: e.str, + Code: http.StatusInternalServerError, + Reason: metav1.StatusReasonInternalError, + Status: metav1.StatusFailure, + } + + if e.innerStatusErr != nil { + s := e.innerStatusErr.Status() + status.Code, status.Reason, status.Status, status.Details = s.Code, s.Reason, s.Status, s.Details + } + return status +} + +func (e *fmtError) Unwrap() error { + return e.inner +} + +func (e *fmtError) Is(target error) bool { + if e.initInnerStatusErr && e.innerStatusErr != nil { + // If we already know the inner status, we can speed up the Is check for apierrors Is functions. These are the most common case. + if err, ok := e.innerStatusErr.(error); ok { + return errors.Is(err, target) + } + } + return errors.Is(e.inner, target) +} + +// Errorf acts like `fmt.Errorf`. Use `%w` to wrap a specific error. +// The returned error will propagate the inner `metav1.Status`, if one exists. Otherwise, an HTTP 500 Internal Server Error will be returned. +// If multiple errors are passed, they will be joined with `errors.Join`, just like `fmt.Errorf`. +func Errorf(format string, args ...any) *fmtError { + // We go via Errorf to only give the %w errors as inner errors. + wrapped := fmt.Errorf(format, args...) + str := wrapped.Error() + err := unwrap(wrapped) + + return &fmtError{ + inner: err, + str: str, + } +} + +// unwrap returns the inner error of an error, if it exists. +// If multiple errors are present, it will errors.Join them. +func unwrap(err error) error { + type singleUnwrapper interface { + Unwrap() error + } + type multiUnwrapper interface { + Unwrap() []error + } + + if err == nil { + return nil + } + if e, ok := err.(singleUnwrapper); ok { + return e.Unwrap() + } + if e, ok := err.(multiUnwrapper); ok { + errs := e.Unwrap() + if len(errs) == 0 { + return err + } + if len(errs) == 1 && errs[0] != nil { + return errs[0] + } + return errors.Join(errs...) + } + return err +} diff --git a/pkg/registry/apis/provisioning/apifmt/error_test.go b/pkg/registry/apis/provisioning/apifmt/error_test.go new file mode 100644 index 00000000000..b685bcad673 --- /dev/null +++ b/pkg/registry/apis/provisioning/apifmt/error_test.go @@ -0,0 +1,97 @@ +package apifmt_test + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/apifmt" + "github.com/stretchr/testify/assert" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestErrorf(t *testing.T) { + t.Parallel() + + for _, fmt := range []string{"1 %v 2 %v", "1 %w 2 %v", "1 %v 2 %w", "1 %w 2 %w"} { + t.Run("error string is formatted appropriately with fmt="+fmt, func(t *testing.T) { + t.Parallel() + + err1 := errors.New("error1") + err2 := errors.New("error2") + + err := apifmt.Errorf(fmt, err1, err2) + assert.Equal(t, "1 error1 2 error2", err.Error()) + }) + } + + t.Run("no inner error defaults to internal server error", func(t *testing.T) { + t.Parallel() + + err := apifmt.Errorf("nothing inside") + + assert.True(t, apierrors.IsInternalError(err), "err is not internal error per apierrors") + assert.Equal(t, int32(http.StatusInternalServerError), err.Status().Code, ".Code") + assert.Equal(t, metav1.StatusReasonInternalError, err.Status().Reason, ".Reason") + assert.Equal(t, metav1.StatusFailure, err.Status().Status, ".Status") + }) + + t.Run("non-apistatus inner error defaults to internal server error", func(t *testing.T) { + t.Parallel() + + inner := errors.New("an inner error") + err := apifmt.Errorf("%w", inner) + + assert.True(t, apierrors.IsInternalError(err), "err is not internal error per apierrors") + assert.Equal(t, int32(http.StatusInternalServerError), err.Status().Code, ".Code") + assert.Equal(t, metav1.StatusReasonInternalError, err.Status().Reason, ".Reason") + assert.Equal(t, metav1.StatusFailure, err.Status().Status, ".Status") + }) + + t.Run("apistatus inner error is used for status", func(t *testing.T) { + t.Parallel() + + inner := apierrors.NewBadRequest("bad request") + err := apifmt.Errorf("%w", inner) + + assert.Equal(t, inner.Status(), err.Status(), "err.Status()") + }) + + t.Run("message is used with inner apistatus error", func(t *testing.T) { + t.Parallel() + + inner := apierrors.NewBadRequest("bad request") + err := apifmt.Errorf("context here: %w", inner) + + status := inner.Status() + status.Message = "context here: bad request" + assert.Equal(t, status, err.Status(), "err.Status()") + assert.Equal(t, "context here: bad request", err.Error(), "err.Error()") + }) + + t.Run("deep apierror is used", func(t *testing.T) { + t.Parallel() + + inner := apierrors.NewBadRequest("bad request") + wrapped := fmt.Errorf("%w", inner) + wrapped = fmt.Errorf("%w", wrapped) + err := apifmt.Errorf("%w", wrapped) + + assert.Equal(t, inner.Status(), err.Status(), "err.Status()") + }) + + t.Run("deep error in multi-unwrap wrapper's apierror is used", func(t *testing.T) { + t.Parallel() + + inner := apierrors.NewBadRequest("bad request") + anotherError := errors.New("not an apierror") + wrapped := errors.Join(fmt.Errorf("this is cool: %w", anotherError), fmt.Errorf("another one: %w", errors.Join(anotherError, inner, anotherError))) + err := apifmt.Errorf("%w", wrapped) + + status := inner.Status() + status.Message = "this is cool: not an apierror\nanother one: not an apierror\nbad request\nnot an apierror" + assert.Equal(t, status, err.Status(), "err.Status()") + }) +} diff --git a/pkg/registry/apis/provisioning/controller/finalizers.go b/pkg/registry/apis/provisioning/controller/finalizers.go new file mode 100644 index 00000000000..1c0437d3fca --- /dev/null +++ b/pkg/registry/apis/provisioning/controller/finalizers.go @@ -0,0 +1,142 @@ +package controller + +import ( + "sort" + "strings" + + "golang.org/x/net/context" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/utils" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +// RemoveOrphanResourcesFinalizer removes everything this repo created +const RemoveOrphanResourcesFinalizer = "remove-orphan-resources" + +// ReleaseOrphanResourcesFinalizer removes the metadata for anything this repo created +const ReleaseOrphanResourcesFinalizer = "release-orphan-resources" + +// CleanFinalizer calls the "OnDelete" function for resource +const CleanFinalizer = "cleanup" + +type finalizer struct { + lister resources.ResourceLister + clientFactory *resources.ClientFactory +} + +func (f *finalizer) process(ctx context.Context, + repo repository.Repository, + finalizers []string, +) error { + logger := logging.FromContext(ctx) + + for _, finalizer := range finalizers { + switch finalizer { + case CleanFinalizer: + // NOTE: the controller loop will never get run unless a finalizer is set + hooks, ok := repo.(repository.Hooks) + if ok { + if err := hooks.OnDelete(ctx); err != nil { + logger.Warn("Error running deletion hooks", "err", err) + } + } + + case ReleaseOrphanResourcesFinalizer: + err := f.processExistingItems(ctx, repo.Config(), + func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error { + _, err := client.Patch(ctx, item.Name, types.JSONPatchType, []byte(`[ + {"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeyManagerKind+`" }, + {"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeyManagerIdentity+`" }, + {"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeySourcePath+`" }, + {"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeySourceChecksum+`" } + ]`), v1.PatchOptions{}) + return err + }) + if err != nil { + return err + } + + case RemoveOrphanResourcesFinalizer: + err := f.processExistingItems(ctx, repo.Config(), + func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error { + return client.Delete(ctx, item.Name, v1.DeleteOptions{}) + }) + if err != nil { + return err + } + + default: + logger.Warn("skipping unknown finalizer", "finalizer", finalizer) + } + } + return nil +} + +// internal iterator to walk the existing items +func (f *finalizer) processExistingItems( + ctx context.Context, + repo *provisioning.Repository, + cb func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error, +) error { + logger := logging.FromContext(ctx) + clients, err := f.clientFactory.Clients(ctx, repo.Namespace) + if err != nil { + return err + } + + items, err := f.lister.List(ctx, repo.Namespace, repo.Name) + if err != nil { + logger.Warn("error listing resources", "error", err) + return err + } + + // Safe deletion order + sortResourceListForDeletion(items) + count := 0 + errors := 0 + + for _, item := range items.Items { + res, _, err := clients.ForResource(schema.GroupVersionResource{ + Group: item.Group, + Resource: item.Resource, + }) + if err != nil { + return err + } + + err = cb(res, &item) + if err != nil { + logger.Warn("error processing item", "name", item.Name, "error", err) + errors++ + } else { + count++ + } + } + logger.Info("processed orphan items", "items", count, "errors", errors) + return nil +} + +func sortResourceListForDeletion(list *provisioning.ResourceList) { + // FIXME: this code should be simplified once unified storage folders support recursive deletion + // Sort by the following logic: + // - Put folders at the end so that we empty them first. + // - Sort folders by depth so that we remove the deepest first + sort.Slice(list.Items, func(i, j int) bool { + switch { + case list.Items[i].Group != folders.RESOURCE: + return true + case list.Items[j].Group != folders.RESOURCE: + return false + default: + return len(strings.Split(list.Items[i].Path, "/")) > len(strings.Split(list.Items[j].Path, "/")) + } + }) +} diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go new file mode 100644 index 00000000000..5a2a18e375b --- /dev/null +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -0,0 +1,532 @@ +package controller + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + client "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" + informer "github.com/grafana/grafana/pkg/generated/informers/externalversions/provisioning/v0alpha1" + listers "github.com/grafana/grafana/pkg/generated/listers/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" +) + +type RepoGetter interface { + // Given a repository configuration, return it as a repository instance + // This will only error for un-recoverable system errors + // the repository instance may or may not be valid/healthy + AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error) +} + +type RepositoryTester interface { + TestRepository(ctx context.Context, repo repository.Repository) (*provisioning.TestResults, error) +} + +const loggerName = "provisioning-repository-controller" + +const ( + maxAttempts = 3 +) + +type queueItem struct { + key string + obj interface{} + attempts int +} + +// RepositoryController controls how and when CRD is established. +type RepositoryController struct { + client client.ProvisioningV0alpha1Interface + resourceLister resources.ResourceLister + repoLister listers.RepositoryLister + repoSynced cache.InformerSynced + parsers *resources.ParserFactory + logger logging.Logger + secrets secrets.Service + dualwrite dualwrite.Service + + jobs jobs.Queue + finalizer *finalizer + + // Converts config to instance + repoGetter RepoGetter + tester RepositoryTester + // To allow injection for testing. + processFn func(item *queueItem) error + enqueueRepository func(obj any) + keyFunc func(obj any) (string, error) + + queue workqueue.TypedRateLimitingInterface[*queueItem] +} + +// NewRepositoryController creates new RepositoryController. +func NewRepositoryController( + provisioningClient client.ProvisioningV0alpha1Interface, + repoInformer informer.RepositoryInformer, + repoGetter RepoGetter, + resourceLister resources.ResourceLister, + parsers *resources.ParserFactory, + tester RepositoryTester, + jobs jobs.Queue, + secrets secrets.Service, + dualwrite dualwrite.Service, +) (*RepositoryController, error) { + rc := &RepositoryController{ + client: provisioningClient, + resourceLister: resourceLister, + repoLister: repoInformer.Lister(), + repoSynced: repoInformer.Informer().HasSynced, + queue: workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.DefaultTypedControllerRateLimiter[*queueItem](), + workqueue.TypedRateLimitingQueueConfig[*queueItem]{ + Name: "provisioningRepositoryController", + }, + ), + repoGetter: repoGetter, + parsers: parsers, + finalizer: &finalizer{ + lister: resourceLister, + clientFactory: parsers.ClientFactory, + }, + tester: tester, + jobs: jobs, + logger: logging.DefaultLogger.With("logger", loggerName), + secrets: secrets, + dualwrite: dualwrite, + } + + _, err := repoInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: rc.enqueue, + UpdateFunc: func(oldObj, newObj interface{}) { + rc.enqueue(newObj) + }, + }) + if err != nil { + return nil, err + } + + rc.processFn = rc.process + rc.enqueueRepository = rc.enqueue + rc.keyFunc = repoKeyFunc + + return rc, nil +} + +func repoKeyFunc(obj any) (string, error) { + repo, ok := obj.(*provisioning.Repository) + if !ok { + return "", fmt.Errorf("expected a Repository but got %T", obj) + } + return cache.DeletionHandlingMetaNamespaceKeyFunc(repo) +} + +// Run starts the RepositoryController. +func (rc *RepositoryController) Run(ctx context.Context, workerCount int) { + defer utilruntime.HandleCrash() + defer rc.queue.ShutDown() + + logger := rc.logger + ctx = logging.Context(ctx, logger) + logger.Info("Starting RepositoryController") + defer logger.Info("Shutting down RepositoryController") + + if !cache.WaitForCacheSync(ctx.Done(), rc.repoSynced) { + return + } + + logger.Info("Starting workers", "count", workerCount) + for i := 0; i < workerCount; i++ { + go wait.UntilWithContext(ctx, rc.runWorker, time.Second) + } + + logger.Info("Started workers") + <-ctx.Done() + logger.Info("Shutting down workers") +} + +func (rc *RepositoryController) runWorker(ctx context.Context) { + for rc.processNextWorkItem(ctx) { + } +} + +func (rc *RepositoryController) enqueue(obj interface{}) { + key, err := rc.keyFunc(obj) + if err != nil { + utilruntime.HandleError(fmt.Errorf("couldn't get key for object: %v", err)) + return + } + + item := queueItem{key: key, obj: obj} + rc.queue.Add(&item) +} + +// processNextWorkItem deals with one key off the queue. +// It returns false when it's time to quit. +func (rc *RepositoryController) processNextWorkItem(ctx context.Context) bool { + item, quit := rc.queue.Get() + if quit { + return false + } + defer rc.queue.Done(item) + + // TODO: should we move tracking work to trace ids instead? + logger := logging.FromContext(ctx).With("work_key", item.key) + logger.Info("RepositoryController processing key") + + err := rc.processFn(item) + if err == nil { + rc.queue.Forget(item) + return true + } + + item.attempts++ + logger = logger.With("error", err, "attempts", item.attempts) + logger.Error("RepositoryController failed to process key") + + if item.attempts >= maxAttempts { + logger.Error("RepositoryController failed too many times") + rc.queue.Forget(item) + return true + } + + if !apierrors.IsServiceUnavailable(err) { + logger.Info("RepositoryController will not retry") + rc.queue.Forget(item) + return true + } else { + logger.Info("RepositoryController will retry as service is unavailable") + } + + utilruntime.HandleError(fmt.Errorf("%v failed with: %v", item, err)) + rc.queue.AddRateLimited(item) + + return true +} + +func (rc *RepositoryController) handleDelete(ctx context.Context, obj *provisioning.Repository) error { + logger := logging.FromContext(ctx) + logger.Info("handle repository delete") + + // Process any finalizers + if len(obj.Finalizers) > 0 { + repo, err := rc.repoGetter.AsRepository(ctx, obj) + if err != nil { + logger.Warn("unable to get repository for cleanup") + } else { + err := rc.finalizer.process(ctx, repo, obj.Finalizers) + if err != nil { + logger.Warn("error running finalizer", "err") + } + } + + // remove the finalizers + _, err = rc.client.Repositories(obj.GetNamespace()). + Patch(ctx, obj.Name, types.JSONPatchType, []byte(`[ + { "op": "remove", "path": "/metadata/finalizers" } + ]`), v1.PatchOptions{ + FieldManager: "repository-controller", + }) + return err // delete will be called again + } + + return nil +} + +func (rc *RepositoryController) shouldCheckHealth(obj *provisioning.Repository) bool { + if obj.Status.Health.Checked == 0 || obj.Generation != obj.Status.ObservedGeneration { + return true + } + + healthAge := time.Since(time.UnixMilli(obj.Status.Health.Checked)) + if obj.Status.Health.Healthy { + return healthAge > time.Minute*5 // when healthy, check every 5 mins + } + + return healthAge > time.Minute // otherwise within a minute +} + +func (rc *RepositoryController) runHealthCheck(ctx context.Context, repo repository.Repository) provisioning.HealthStatus { + logger := logging.FromContext(ctx) + logger.Info("running health check") + res, err := rc.tester.TestRepository(ctx, repo) + if err != nil { + res = &provisioning.TestResults{ + Success: false, + Errors: []string{ + "error running test repository", + err.Error(), + }, + } + } + + healthStatus := provisioning.HealthStatus{ + Healthy: res.Success, + Checked: time.Now().UnixMilli(), + Message: res.Errors, + } + logger.Info("health check completed", "status", healthStatus) + + return healthStatus +} + +func (rc *RepositoryController) shouldResync(obj *provisioning.Repository) bool { + // don't trigger resync if a sync was never started + if obj.Status.Sync.Finished == 0 && obj.Status.Sync.State == "" { + return false + } + + syncAge := time.Since(time.UnixMilli(obj.Status.Sync.Finished)) + syncInterval := time.Duration(obj.Spec.Sync.IntervalSeconds) * time.Second + tolerance := time.Second + + // HACK: how would this work in a multi-tenant world or under heavy load? + // It will start queueing up jobs and we will have to deal with that + pendingForTooLong := syncAge >= syncInterval/2 && obj.Status.Sync.State == provisioning.JobStatePending + isRunning := obj.Status.Sync.State == provisioning.JobStateWorking + + return obj.Spec.Sync.Enabled && syncAge >= (syncInterval-tolerance) && !pendingForTooLong && !isRunning +} + +func (rc *RepositoryController) runHooks(ctx context.Context, repo repository.Repository, obj *provisioning.Repository) (*provisioning.WebhookStatus, error) { + logger := logging.FromContext(ctx) + hooks, _ := repo.(repository.Hooks) + if hooks == nil || obj.Generation == obj.Status.ObservedGeneration { + return nil, nil + } + + if obj.Status.ObservedGeneration < 1 { + logger.Info("handle repository create") + webhookStatus, err := hooks.OnCreate(ctx) + if err != nil { + return nil, fmt.Errorf("error running OnCreate: %w", err) + } + return webhookStatus, nil + } + + logger.Info("handle repository spec update", "Generation", obj.Generation, "ObservedGeneration", obj.Status.ObservedGeneration) + webhookStatus, err := hooks.OnUpdate(ctx) + if err != nil { + return nil, fmt.Errorf("error running OnUpdate: %w", err) + } + + return webhookStatus, nil +} + +func (rc *RepositoryController) determineSyncStrategy(ctx context.Context, obj *provisioning.Repository, shouldResync bool, healthStatus provisioning.HealthStatus) *provisioning.SyncJobOptions { + logger := logging.FromContext(ctx) + + switch { + case !obj.Spec.Sync.Enabled: + logger.Info("skip sync as it's disabled") + return nil + case !healthStatus.Healthy: + logger.Info("skip sync for unhealthy repository") + return nil + case dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, rc.dualwrite): + logger.Info("skip sync as we are reading from legacy storage") + return nil + case healthStatus.Healthy != obj.Status.Health.Healthy: + logger.Info("repository became healthy, full resync") + return &provisioning.SyncJobOptions{} + case obj.Status.ObservedGeneration < 1: + logger.Info("full sync for new repository") + return &provisioning.SyncJobOptions{} + case obj.Generation != obj.Status.ObservedGeneration: + logger.Info("full sync for spec change") + return &provisioning.SyncJobOptions{} + case shouldResync: + logger.Info("incremental sync for sync interval") + return &provisioning.SyncJobOptions{Incremental: true} + default: + return nil + } +} + +func (rc *RepositoryController) addSyncJob(ctx context.Context, obj *provisioning.Repository, syncOptions *provisioning.SyncJobOptions) error { + job, err := rc.jobs.Insert(ctx, &provisioning.Job{ + ObjectMeta: v1.ObjectMeta{ + Namespace: obj.Namespace, + }, + Spec: provisioning.JobSpec{ + Repository: obj.GetName(), + Action: provisioning.JobActionSync, + Pull: syncOptions, + }, + }) + if err != nil { + // FIXME: should we update the status of the repository if we fail to add the job? + return fmt.Errorf("error adding sync job: %w", err) + } + + logging.FromContext(ctx).Info("sync job triggered", "job", job.Name) + return nil +} + +func (rc *RepositoryController) patchStatus(ctx context.Context, obj *provisioning.Repository, patchOperations []map[string]interface{}) error { + if len(patchOperations) == 0 { + return nil + } + + patch, err := json.Marshal(patchOperations) + if err != nil { + return fmt.Errorf("error encoding status patch: %w", err) + } + + _, err = rc.client.Repositories(obj.GetNamespace()). + Patch(ctx, obj.Name, types.JSONPatchType, patch, v1.PatchOptions{}, "status") + if err != nil { + return fmt.Errorf("error applying status patch: %w", err) + } + + return nil +} + +func (rc *RepositoryController) determineSyncStatus(obj *provisioning.Repository, syncOptions *provisioning.SyncJobOptions) *provisioning.SyncStatus { + const unhealthyMessage = "Repository is unhealthy" + + hasUnhealthyMessage := len(obj.Status.Sync.Message) > 0 && obj.Status.Sync.Message[0] == unhealthyMessage + switch { + case syncOptions != nil: + return &provisioning.SyncStatus{ + State: provisioning.JobStatePending, + LastRef: obj.Status.Sync.LastRef, + Started: time.Now().UnixMilli(), + } + case obj.Status.Health.Healthy && hasUnhealthyMessage: // if the repository is healthy and the message is set, clear it + // FIXME: is this the clearest way to do this? Should we introduce another status or way of way of handling more + // specific errors? + return &provisioning.SyncStatus{ + LastRef: obj.Status.Sync.LastRef, + } + case !obj.Status.Health.Healthy && !hasUnhealthyMessage: // if the repository is unhealthy and the message is not already set, set it + return &provisioning.SyncStatus{ + State: provisioning.JobStateError, + Message: []string{unhealthyMessage}, + LastRef: obj.Status.Sync.LastRef, + } + default: + return nil + } +} + +//nolint:gocyclo +func (rc *RepositoryController) process(item *queueItem) error { + logger := rc.logger.With("key", item.key) + + namespace, name, err := cache.SplitMetaNamespaceKey(item.key) + if err != nil { + return err + } + + obj, err := rc.repoLister.Repositories(namespace).Get(name) + switch { + case apierrors.IsNotFound(err): + return errors.New("repository not found in cache") + case err != nil: + return err + } + + ctx, _, err := identity.WithProvisioningIdentity(context.Background(), namespace) + if err != nil { + return err + } + ctx = request.WithNamespace(ctx, namespace) + logger = logger.WithContext(ctx) + + if obj.DeletionTimestamp != nil { + return rc.handleDelete(ctx, obj) + } + + shouldResync := rc.shouldResync(obj) + shouldCheckHealth := rc.shouldCheckHealth(obj) + hasSpecChanged := obj.Generation != obj.Status.ObservedGeneration + patchOperations := []map[string]interface{}{} + + // Determine the main triggering condition + switch { + case hasSpecChanged: + logger.Info("spec changed", "Generation", obj.Generation, "ObservedGeneration", obj.Status.ObservedGeneration) + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/observedGeneration", + "value": obj.Generation, + }) + case shouldResync: + logger.Info("sync interval triggered", "sync_interval", time.Duration(obj.Spec.Sync.IntervalSeconds)*time.Second, "sync_status", obj.Status.Sync) + case shouldCheckHealth: + logger.Info("health is stale", "health_status", obj.Status.Health.Healthy) + default: + logger.Info("skipping as conditions are not met", "status", obj.Status, "generation", obj.Generation, "sync_spec", obj.Spec.Sync) + return nil + } + + repo, err := rc.repoGetter.AsRepository(ctx, obj) + if err != nil { + return fmt.Errorf("unable to create repository from configuration: %w", err) + } + + healthStatus := obj.Status.Health + if shouldCheckHealth { + healthStatus = rc.runHealthCheck(ctx, repo) + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/health", + "value": healthStatus, + }) + } + + // Run hooks + webhookStatus, err := rc.runHooks(ctx, repo, obj) + switch { + case err != nil: + return err + case webhookStatus != nil: + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/webhook", + "value": webhookStatus, + }) + } + + // determine the sync strategy and sync status to apply + syncOptions := rc.determineSyncStrategy(ctx, obj, shouldResync, healthStatus) + if syncStatus := rc.determineSyncStatus(obj, syncOptions); syncStatus != nil { + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync", + "value": syncStatus, + }) + } + + // Apply all patch operations + if err := rc.patchStatus(ctx, obj, patchOperations); err != nil { + return err + } + + // Trigger sync job after we have applied all patch operations + if syncOptions != nil { + if err := rc.addSyncJob(ctx, obj, syncOptions); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/export.go b/pkg/registry/apis/provisioning/export.go new file mode 100644 index 00000000000..71e71e4678c --- /dev/null +++ b/pkg/registry/apis/provisioning/export.go @@ -0,0 +1,83 @@ +package provisioning + +import ( + "context" + "net/http" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +type exportConnector struct { + repoGetter RepoGetter + jobs jobs.Queue +} + +func (*exportConnector) New() runtime.Object { + return &provisioning.Job{} +} + +func (*exportConnector) Destroy() {} + +func (*exportConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (c *exportConnector) ProducesObject(verb string) any { + return c.New() +} + +func (*exportConnector) ConnectMethods() []string { + return []string{http.MethodPost} +} + +func (*exportConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (c *exportConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + repo, err := c.repoGetter.GetRepository(ctx, name) + if err != nil { + return nil, err + } + cfg := repo.Config() + if err := repository.IsWriteAllowed(cfg, ""); err != nil { + return nil, err + } + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + options := &provisioning.ExportJobOptions{} + if err := unmarshalJSON(r, defaultMaxBodySize, options); err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + job, err := c.jobs.Insert(ctx, &provisioning.Job{ + ObjectMeta: v1.ObjectMeta{ + Namespace: cfg.Namespace, + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionExport, + Repository: cfg.Name, + Push: options, + }, + }) + if err != nil { + responder.Error(err) + } else { + responder.Object(http.StatusAccepted, job) + } + }), 30*time.Second), nil +} + +var ( + _ rest.Connecter = (*exportConnector)(nil) + _ rest.Storage = (*exportConnector)(nil) + _ rest.StorageMetadata = (*exportConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/filepath.go b/pkg/registry/apis/provisioning/filepath.go new file mode 100644 index 00000000000..d8b9146e49e --- /dev/null +++ b/pkg/registry/apis/provisioning/filepath.go @@ -0,0 +1,16 @@ +package provisioning + +import ( + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +func pathAfterPrefix(urlPath, prefix string) (string, error) { + idx := strings.Index(urlPath, prefix) + if idx == -1 { + return "", apierrors.NewBadRequest("invalid request path") + } + + return strings.TrimPrefix(urlPath[idx+len(prefix):], "/"), nil +} diff --git a/pkg/registry/apis/provisioning/filepath_test.go b/pkg/registry/apis/provisioning/filepath_test.go new file mode 100644 index 00000000000..3758c9a48ba --- /dev/null +++ b/pkg/registry/apis/provisioning/filepath_test.go @@ -0,0 +1,79 @@ +package provisioning + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPathAfterPrefix(t *testing.T) { + tests := []struct { + name string + urlPath string + prefix string + want string + expectError bool + }{ + { + name: "basic path with prefix", + urlPath: "/a/b/prefix/c", + prefix: "/prefix", + want: "c", + expectError: false, + }, + { + name: "path with multiple prefix occurrences", + urlPath: "/a/prefix/b/prefix/c", + prefix: "/prefix", + want: "b/prefix/c", + expectError: false, + }, + { + name: "path without prefix", + urlPath: "/a/b/c", + prefix: "/prefix", + want: "", + expectError: true, + }, + { + name: "empty path", + urlPath: "", + prefix: "/prefix", + want: "", + expectError: true, + }, + { + name: "empty prefix", + urlPath: "/a/b/c/d", + prefix: "", + want: "a/b/c/d", + expectError: false, + }, + { + name: "prefix at start of path", + urlPath: "/prefix/rest/of/path", + prefix: "/prefix", + want: "rest/of/path", + expectError: false, + }, + { + name: "prefix in middle with special chars", + urlPath: "/a/b-c/prefix/d_e/f", + prefix: "/prefix", + want: "d_e/f", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := pathAfterPrefix(tt.urlPath, tt.prefix) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go new file mode 100644 index 00000000000..ffd6d678e06 --- /dev/null +++ b/pkg/registry/apis/provisioning/files.go @@ -0,0 +1,386 @@ +package provisioning + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +const ( + // Files endpoint max size for dashboards etc (5MB) + filesMaxBodySize = 5 * 1024 * 1024 +) + +type filesConnector struct { + getter RepoGetter + parsers *resources.ParserFactory +} + +func (*filesConnector) New() runtime.Object { + // This is added as the "ResponseType" regardless what ProducesObject() returns + return &provisioning.ResourceWrapper{} +} + +func (*filesConnector) Destroy() {} + +func (*filesConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (*filesConnector) ProducesObject(verb string) any { + return &provisioning.ResourceWrapper{} +} + +func (*filesConnector) ConnectMethods() []string { + return []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete} +} + +func (*filesConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, true, "" // true adds the {path} component +} + +// TODO: document the synchronous write and delete on the API Spec +// TODO: Move dual write logic to `resources` package and keep this connector simple +func (s *filesConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + logger := logging.FromContext(ctx).With("logger", "files-connector", "repository_name", name) + ctx = logging.Context(ctx, logger) + repo, err := s.getter.GetRepository(ctx, name) + if err != nil { + logger.Debug("failed to find repository", "error", err) + return nil, err + } + + reader, ok := repo.(repository.Reader) + if !ok { + return nil, apierrors.NewBadRequest("repository does not support read") + } + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + ref := query.Get("ref") + message := query.Get("message") + logger := logger.With("url", r.URL.Path, "ref", ref, "message", message) + ctx := logging.Context(r.Context(), logger) + + filePath, err := pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/files", name)) + if err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + + if err := resources.IsPathSupported(filePath); err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + + isDir := safepath.IsDir(filePath) + if r.Method == http.MethodGet && isDir { + // TODO: Implement folder navigation + if len(filePath) > 0 { + responder.Error(apierrors.NewBadRequest("folder navigation not yet supported")) + return + } + + // TODO: Add pagination + rsp, err := reader.ReadTree(ctx, ref) + if err != nil { + responder.Error(err) + return + } + + files := &provisioning.FileList{} + for _, v := range rsp { + if !v.Blob { + continue // folder item + } + files.Items = append(files.Items, provisioning.FileItem{ + Path: v.Path, + Size: v.Size, + Hash: v.Hash, + }) + } + responder.Object(http.StatusOK, files) + return + } + + if filePath == "" { + responder.Error(apierrors.NewBadRequest("path is required")) + return + } + + // TODO: Implement folder delete + if r.Method == http.MethodDelete && isDir { + responder.Error(apierrors.NewBadRequest("folder navigation not yet supported")) + return + } + + var obj *provisioning.ResourceWrapper + code := http.StatusOK + switch r.Method { + case http.MethodGet: + code, obj, err = s.doRead(ctx, reader, filePath, ref) + case http.MethodPost: + obj, err = s.doWrite(ctx, false, repo, filePath, ref, message, r) + case http.MethodPut: + // TODO: document in API specification + if isDir { + err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) + } else { + obj, err = s.doWrite(ctx, true, repo, filePath, ref, message, r) + } + case http.MethodDelete: + // TODO: limit file size + obj, err = s.doDelete(ctx, repo, filePath, ref, message) + default: + err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) + } + + if err != nil { + logger.Debug("got an error after processing request", "error", err) + responder.Error(err) + return + } + + // something failed + if len(obj.Errors) > 0 { + code = http.StatusInternalServerError + } + + logger.Debug("request resulted in valid object", "object", obj) + responder.Object(code, obj) + }), 30*time.Second), nil +} + +func (s *filesConnector) doRead(ctx context.Context, repo repository.Reader, path string, ref string) (int, *provisioning.ResourceWrapper, error) { + info, err := repo.Read(ctx, path, ref) + if err != nil { + return 0, nil, err + } + + parser, err := s.parsers.GetParser(ctx, repo) + if err != nil { + return 0, nil, err + } + + parsed, err := parser.Parse(ctx, info, true) + if err != nil { + return 0, nil, err + } + + // GVR will exist for anything we can actually save + // TODO: Add known error in parser for unsupported resource + if parsed.GVR == nil { + if parsed.GVK != nil { + //nolint:govet + parsed.Errors = append(parsed.Errors, fmt.Errorf("unknown resource for Kind: %s", parsed.GVK.Kind)) + } else { + parsed.Errors = append(parsed.Errors, fmt.Errorf("unknown resource")) + } + } + + code := http.StatusOK + if len(parsed.Errors) > 0 { + code = http.StatusNotAcceptable + } + return code, parsed.AsResourceWrapper(), nil +} + +func (s *filesConnector) doWrite(ctx context.Context, update bool, repo repository.Repository, path string, ref string, message string, req *http.Request) (*provisioning.ResourceWrapper, error) { + if err := repository.IsWriteAllowed(repo.Config(), ref); err != nil { + return nil, err + } + + writer, ok := repo.(repository.ReaderWriter) + if !ok { + return nil, apierrors.NewBadRequest("repository does not support read-writing") + } + + parser, err := s.parsers.GetParser(ctx, writer) + if err != nil { + return nil, err + } + + defer func() { _ = req.Body.Close() }() + if safepath.IsDir(path) { + return s.doCreateFolder(ctx, writer, path, ref, message, parser) + } + + data, err := readBody(req, filesMaxBodySize) + if err != nil { + return nil, err + } + + info := &repository.FileInfo{ + Data: data, + Path: path, + Ref: ref, + } + + // TODO: improve parser to parse out of reader + parsed, err := parser.Parse(ctx, info, true) + if err != nil { + if errors.Is(err, resources.ErrUnableToReadResourceBytes) { + return nil, apierrors.NewBadRequest("unable to read the request as a resource") + } + return nil, err + } + + // GVR will exist for anything we can actually save + // TODO: Add known error in parser for unsupported resource + if parsed.GVR == nil { + return nil, apierrors.NewBadRequest("The payload does not map to a known resource") + } + + // Do not write if any errors exist + if len(parsed.Errors) > 0 { + return parsed.AsResourceWrapper(), err + } + + data, err = parsed.ToSaveBytes() + if err != nil { + return nil, err + } + + if update { + err = writer.Update(ctx, path, ref, data, message) + } else { + err = writer.Create(ctx, path, ref, data, message) + } + if err != nil { + return nil, err + } + + // Directly update the grafana database + // Behaves the same running sync after writing + if ref == "" { + if parsed.Existing == nil { + parsed.Upsert, err = parsed.Client.Create(ctx, parsed.Obj, metav1.CreateOptions{}) + if err != nil { + parsed.Errors = append(parsed.Errors, err) + } + } else { + parsed.Upsert, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{}) + if err != nil { + parsed.Errors = append(parsed.Errors, err) + } + } + } + + return parsed.AsResourceWrapper(), err +} + +func (s *filesConnector) doCreateFolder(ctx context.Context, repo repository.Writer, path string, ref string, message string, parser *resources.Parser) (*provisioning.ResourceWrapper, error) { + client, err := parser.Clients().Folder() + if err != nil { + return nil, err + } + manager := resources.NewFolderManager(repo, client) + + // Now actually create the folder + if err := repo.Create(ctx, path, ref, nil, message); err != nil { + return nil, fmt.Errorf("failed to create folder: %w", err) + } + + cfg := repo.Config() + wrap := &provisioning.ResourceWrapper{ + Path: path, + Ref: ref, + Repository: provisioning.ResourceRepositoryInfo{ + Type: cfg.Spec.Type, + Namespace: cfg.Namespace, + Name: cfg.Name, + Title: cfg.Spec.Title, + }, + Resource: provisioning.ResourceObjects{ + Action: provisioning.ResourceActionCreate, + }, + } + + if ref == "" { + folderName, err := manager.EnsureFolderPathExist(ctx, path) + if err != nil { + return nil, err + } + + current, err := manager.GetFolder(ctx, folderName) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err // unable to check if the folder exists + } + wrap.Resource.Upsert = v0alpha1.Unstructured{ + Object: current.Object, + } + } + + return wrap, nil +} + +// Deletes a file from the repository and the Grafana database. +// If the path is a folder, it will return an error. +// If the file is not parsable, it will return an error. +func (s *filesConnector) doDelete(ctx context.Context, repo repository.Repository, path string, ref string, message string) (*provisioning.ResourceWrapper, error) { + if err := repository.IsWriteAllowed(repo.Config(), ref); err != nil { + return nil, err + } + + // Read the existing value + access, ok := repo.(repository.ReaderWriter) + if !ok { + return nil, fmt.Errorf("repository is not read+writeable") + } + + file, err := access.Read(ctx, path, ref) + if err != nil { + return nil, err // unable to read value + } + + parser, err := s.parsers.GetParser(ctx, access) + if err != nil { + return nil, err // unable to read value + } + + // TODO: document in API specification + // We can only delete parsable things + parsed, err := parser.Parse(ctx, file, false) + if err != nil { + return nil, err // unable to read value + } + + parsed.Action = provisioning.ResourceActionDelete + wrap := parsed.AsResourceWrapper() + + // Now delete the file + err = access.Delete(ctx, path, ref, message) + if err != nil { + return nil, err + } + + // Delete the file in the grafana database + if ref == "" { + err = parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{}) + if apierrors.IsNotFound(err) { + err = nil // ignorable + } + } + + return wrap, err +} + +var ( + _ rest.Storage = (*filesConnector)(nil) + _ rest.Connecter = (*filesConnector)(nil) + _ rest.StorageMetadata = (*filesConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/history.go b/pkg/registry/apis/provisioning/history.go new file mode 100644 index 00000000000..cc45f63f32d --- /dev/null +++ b/pkg/registry/apis/provisioning/history.go @@ -0,0 +1,97 @@ +package provisioning + +import ( + "context" + "fmt" + "net/http" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +type historySubresource struct { + repoGetter RepoGetter +} + +func (h *historySubresource) New() runtime.Object { + // This is added as the "ResponseType" regardless what ProducesObject() returns + return &provisioning.HistoryList{} +} + +func (h *historySubresource) Destroy() {} + +func (h *historySubresource) NamespaceScoped() bool { + return true +} + +func (h *historySubresource) GetSingularName() string { + return "History" +} + +func (h *historySubresource) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (h *historySubresource) ProducesObject(verb string) runtime.Object { + return &provisioning.HistoryList{} +} + +func (h *historySubresource) ConnectMethods() []string { + return []string{http.MethodGet} +} + +func (h *historySubresource) NewConnectOptions() (runtime.Object, bool, string) { + return nil, true, "" // true adds the {path} component +} + +func (h *historySubresource) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + logger := logging.FromContext(ctx).With("logger", "history-subresource") + ctx = logging.Context(ctx, logger) + repo, err := h.repoGetter.GetRepository(ctx, name) + if err != nil { + logger.Debug("failed to find repository", "error", err) + return nil, err + } + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + versioned, ok := repo.(repository.Versioned) + if !ok { + responder.Error(apierrors.NewBadRequest("this repository does not support history")) + return + } + + query := r.URL.Query() + ref := query.Get("ref") + + filePath, err := pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/history/", name)) + if err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + + if err := resources.IsPathSupported(filePath); err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + + logger = logger.With("ref", ref, "path", filePath) + ctx = logging.Context(r.Context(), logger) + + // TODO: Add history pagination + commits, err := versioned.History(ctx, filePath, ref) + if err != nil { + logger.Debug("failed to get history", "error", err) + responder.Error(err) + return + } + + responder.Object(http.StatusOK, &provisioning.HistoryList{Items: commits}) + }), 30*time.Second), nil +} diff --git a/pkg/registry/apis/provisioning/jobs/driver.go b/pkg/registry/apis/provisioning/jobs/driver.go new file mode 100644 index 00000000000..f158dfd83e4 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/driver.go @@ -0,0 +1,211 @@ +package jobs + +import ( + "context" + "errors" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/apifmt" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// Store is an abstraction for the storage API. +// This exists to allow for unit testing. +type Store interface { + // Claim takes a job from storage, marks it as ours, and returns it. + // + // Any job which has not been claimed by another worker is fair game. + // + // If err is not nil, the job and rollback values are always nil. + // The err may be ErrNoJobs if there are no jobs to claim. + Claim(ctx context.Context) (job *provisioning.Job, rollback func(), err error) + + // Complete marks a job as completed and moves it to the historic job store. + // When in the historic store, there is no more claim on the job. + Complete(ctx context.Context, job *provisioning.Job) error + + // Cleanup should be called periodically to clean up abandoned jobs. + // An abandoned job is one that has been claimed by a worker, but the worker has not updated the job in a while. + Cleanup(ctx context.Context) error + + // InsertNotifications returns a channel that will have a value sent to it when a new job is inserted. + // This is used to wake up the job driver when a new job is inserted. + InsertNotifications() chan struct{} + + // Update saves the job back to the store. + Update(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) +} + +var _ Store = (*persistentStore)(nil) + +// jobDriver drives jobs to completion and manages the job queue. +// There may be multiple jobDrivers running in parallel. +// The jobDriver deals with cleaning up upon death and ensuring that jobs remain claimable. +type jobDriver struct { + // Timeout for processing a job. This should be the same or less than a claim expiry. + timeout time.Duration + // CleanupInterval is the time between cleanup runs. + cleanupInterval time.Duration + // JobInterval is the time between job ticks. This should be relatively low. + jobInterval time.Duration + + // Store is the job storage backend. + store Store + // RepoGetter lets us access repositories to pass to the worker. + repoGetter RepoGetter + + // Workers process the job. + // Only the first worker who supports the job will process it; the rest are ignored. + workers []Worker +} + +func NewJobDriver( + timeout, cleanupInterval, jobInterval time.Duration, + store Store, + repoGetter RepoGetter, + workers ...Worker, +) *jobDriver { + return &jobDriver{ + timeout: timeout, + cleanupInterval: cleanupInterval, + jobInterval: jobInterval, + store: store, + repoGetter: repoGetter, + workers: workers, + } +} + +// Run drives jobs to completion. This is a blocking function. +// It will run until the context is canceled. +// This is a thread-safe function; it may be called from multiple goroutines. +func (d *jobDriver) Run(ctx context.Context) { + cleanupTicker := time.NewTicker(d.cleanupInterval) + defer cleanupTicker.Stop() + + jobTicker := time.NewTicker(d.jobInterval) + defer jobTicker.Stop() + + logger := logging.FromContext(ctx).With("logger", "job-driver") + ctx = logging.Context(ctx, logger) + ctx, _, err := identity.WithProvisioningIdentity(ctx, "*") // "*" grants us access to all namespaces. + if err != nil { + logger.Error("failed to grant provisioning identity; this will panic!", "error", err) + panic("unreachable?: failed to grant provisioning identity: " + err.Error()) + } + + // Drive without waiting on startup. + d.startDriving(ctx) + + for { + select { + case <-cleanupTicker.C: + if err := d.store.Cleanup(ctx); err != nil { + logger.Error("failed to cleanup jobs", "error", err) + } + case <-jobTicker.C: + d.startDriving(ctx) + case <-d.store.InsertNotifications(): + d.startDriving(ctx) + } + } +} + +func (d *jobDriver) startDriving(ctx context.Context) { + timeoutCtx, cancel := context.WithTimeout(ctx, d.timeout) + defer cancel() + for timeoutCtx.Err() == nil { + if err := d.drive(timeoutCtx); err != nil { + if !errors.Is(err, context.Canceled) && !errors.Is(err, ErrNoJobs) { + logging.FromContext(ctx).Error("failed to drive jobs", "error", err) + } + break + } + } +} + +func (d *jobDriver) drive(ctx context.Context) error { + logger := logging.FromContext(ctx) + + // Claim a job to work on. + job, rollback, err := d.store.Claim(ctx) + if err != nil { + return apifmt.Errorf("failed to claim job: %w", err) + } + // Ensure that the job is cleaned up if we fail to complete it. + // The rollback function does not care about cancellations. + defer rollback() + + logger = logger.With("job", job.GetName(), "namespace", job.GetNamespace()) + ctx = logging.Context(ctx, logger) + logger.Debug("claimed a job") + + // Now that we have a job, we need to augment our namespace to grant ourselves permission to work on it. + // Incidentally, this also limits our permissions to only the namespace of the job. + ctx = request.WithNamespace(ctx, job.GetNamespace()) + ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) + if err != nil { + return apifmt.Errorf("failed to grant provisioning identity: %w", err) + } + + // Process the job. + start := time.Now() + err = d.processJob(ctx, job) // NOTE: We pass in a pointer here such that the job status can be kept in Complete without re-fetching. + end := time.Now() + logger.Debug("job processed", "duration", end.Sub(start), "error", err) + + if err != nil { + return apifmt.Errorf("failed to process job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + + // Mark the job as completed. + if err := d.store.Complete(ctx, job); err != nil { + return apifmt.Errorf("failed to complete job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + logger.Debug("job completed") + + return nil +} + +func (d *jobDriver) processJob(ctx context.Context, job *provisioning.Job) error { + for _, worker := range d.workers { + if !worker.IsSupported(ctx, *job) { + continue + } + + repo, err := d.repoGetter.GetRepository(ctx, job.Spec.Repository) + if err != nil { + return apifmt.Errorf("failed to get repository '%s': %w", job.Spec.Repository, err) + } + + recorder := newJobProgressRecorder(d.onProgress(job)) + + err = worker.Process(ctx, repo, *job, recorder) + if err != nil { + return apifmt.Errorf("worker failed to process job: %w", err) + } + + job.Status = recorder.Complete(ctx, err) + + return nil + } + + return apifmt.Errorf("no workers were registered to handle the job") +} + +func (d *jobDriver) onProgress(job *provisioning.Job) ProgressFn { + return func(ctx context.Context, status provisioning.JobStatus) error { + logging.FromContext(ctx).Debug("job progress", "status", status) + job.Status = status + + updated, err := d.store.Update(ctx, job) + if err != nil { + return apifmt.Errorf("failed to update job: %w", err) + } + + *job = *updated + return nil + } +} diff --git a/pkg/registry/apis/provisioning/jobs/export/folders.go b/pkg/registry/apis/provisioning/jobs/export/folders.go new file mode 100644 index 00000000000..d8fe746cff0 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/folders.go @@ -0,0 +1,96 @@ +package export + +import ( + "context" + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +// FIXME: revise logging in this method +func (r *exportJob) loadFolders(ctx context.Context) error { + logger := r.logger + r.progress.SetMessage(ctx, "reading folder tree") + + repoName := r.target.Config().Name + + // TODO: should this be logging or message or both? + r.progress.SetMessage(ctx, "read folder tree from unified storage") + client, err := r.client.Folder() + if err != nil { + return err + } + + rawList, err := client.List(ctx, metav1.ListOptions{Limit: 10000}) + if err != nil { + return fmt.Errorf("failed to list folders: %w", err) + } + if rawList.GetContinue() != "" { + return fmt.Errorf("unable to list all folders in one request: %s", rawList.GetContinue()) + } + + for _, item := range rawList.Items { + err = r.folderTree.AddUnstructured(&item, repoName) + if err != nil { + r.progress.Record(ctx, jobs.JobResourceResult{ + Name: item.GetName(), + Resource: folders.RESOURCE, + Group: folders.GROUP, + Error: err, + }) + } + } + + // create folders first is required so that empty folders exist when finished + r.progress.SetMessage(ctx, "write folders") + + err = r.folderTree.Walk(ctx, func(ctx context.Context, folder resources.Folder) error { + p := folder.Path + if r.path != "" { + p = safepath.Join(r.path, p) + } + logger := logger.With("path", p) + + result := jobs.JobResourceResult{ + Name: folder.ID, + Resource: folders.RESOURCE, + Group: folders.GROUP, + Path: p, + } + + _, err := r.target.Read(ctx, p, r.ref) + if err != nil && !(errors.Is(err, repository.ErrFileNotFound) || apierrors.IsNotFound(err)) { + result.Error = fmt.Errorf("failed to check if folder exists before writing: %w", err) + return result.Error + } else if err == nil { + logger.Info("folder already exists") + result.Action = repository.FileActionIgnored + r.progress.Record(ctx, result) + return nil + } + + result.Action = repository.FileActionCreated + msg := fmt.Sprintf("export folder %s", p) + // Create with an empty body will make a folder (or .keep file if unsupported) + if err := r.target.Create(ctx, p, r.ref, nil, msg); err != nil { + result.Error = fmt.Errorf("failed to write folder in repo: %w", err) + r.progress.Record(ctx, result) + return result.Error + } + + r.progress.Record(ctx, result) + return nil + }) + if err != nil { + return fmt.Errorf("failed to write folders: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/export/job.go b/pkg/registry/apis/provisioning/jobs/export/job.go new file mode 100644 index 00000000000..eb7f229d42d --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/job.go @@ -0,0 +1,45 @@ +package export + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +// ExportJob holds all context for a running job +type exportJob struct { + logger logging.Logger + client *resources.ResourceClients // Read from + target repository.ReaderWriter // Write to + namespace string + + progress jobs.JobProgressRecorder + folderTree *resources.FolderTree + + path string // from options (now clean+safe) + ref string // from options (only git) + keepIdentifier bool +} + +func newExportJob(ctx context.Context, + target repository.ReaderWriter, + options provisioning.ExportJobOptions, + clients *resources.ResourceClients, + progress jobs.JobProgressRecorder, +) *exportJob { + return &exportJob{ + namespace: target.Config().Namespace, + target: target, + client: clients, + logger: logging.FromContext(ctx), + progress: progress, + path: options.Path, + ref: options.Branch, + keepIdentifier: options.Identifier, + folderTree: resources.NewEmptyFolderTree(), + } +} diff --git a/pkg/registry/apis/provisioning/jobs/export/resources.go b/pkg/registry/apis/provisioning/jobs/export/resources.go new file mode 100644 index 00000000000..2cfbf138e11 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/resources.go @@ -0,0 +1,149 @@ +package export + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +func (r *exportJob) loadResources(ctx context.Context) error { + kinds := []schema.GroupVersionResource{{ + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + Version: "v1alpha1", + }} + + for _, kind := range kinds { + r.progress.SetMessage(ctx, fmt.Sprintf("reading %s resource", kind.Resource)) + if err := r.loadResourcesFromAPIServer(ctx, kind); err != nil { + return fmt.Errorf("error loading %s %w", kind.Resource, err) + } + } + return nil +} + +func (r *exportJob) loadResourcesFromAPIServer(ctx context.Context, kind schema.GroupVersionResource) error { + client, _, err := r.client.ForResource(kind) + if err != nil { + return err + } + + var continueToken string + for { + list, err := client.List(ctx, metav1.ListOptions{Limit: 100, Continue: continueToken}) + if err != nil { + return fmt.Errorf("error executing list: %w", err) + } + + for _, item := range list.Items { + r.progress.Record(ctx, r.write(ctx, &item)) + if err := r.progress.TooManyErrors(); err != nil { + return err + } + } + + continueToken = list.GetContinue() + if continueToken == "" { + break + } + } + + return nil +} + +func (r *exportJob) write(ctx context.Context, obj *unstructured.Unstructured) jobs.JobResourceResult { + gvk := obj.GroupVersionKind() + result := jobs.JobResourceResult{ + Name: obj.GetName(), + Resource: gvk.Kind, + Group: gvk.Group, + Action: repository.FileActionCreated, + } + + if err := ctx.Err(); err != nil { + result.Error = fmt.Errorf("context error: %w", err) + return result + } + + meta, err := utils.MetaAccessor(obj) + if err != nil { + result.Error = fmt.Errorf("extract meta accessor: %w", err) + return result + } + + // Message from annotations + commitMessage := meta.GetMessage() + if commitMessage == "" { + g := meta.GetGeneration() + if g > 0 { + commitMessage = fmt.Sprintf("Generation: %d", g) + } else { + commitMessage = "exported from grafana" + } + } + + name := meta.GetName() + manager, _ := meta.GetManagerProperties() + if manager.Identity == r.target.Config().GetName() { + result.Action = repository.FileActionIgnored + return result + } + + title := meta.FindTitle("") + if title == "" { + title = name + } + folder := meta.GetFolder() + + // Get the absolute path of the folder + fid, ok := r.folderTree.DirPath(folder, "") + if !ok { + // FIXME: Shouldn't this fail instead? + fid = resources.Folder{ + Path: "__folder_not_found/" + slugify.Slugify(folder), + } + r.logger.Error("folder of item was not in tree of repository") + } + + result.Path = fid.Path + + // Clear the metadata + delete(obj.Object, "metadata") + + if r.keepIdentifier { + meta.SetName(name) // keep the identifier in the metadata + } + + body, err := json.MarshalIndent(obj.Object, "", " ") + if err != nil { + result.Error = fmt.Errorf("failed to marshal dashboard: %w", err) + return result + } + + fileName := slugify.Slugify(title) + ".json" + if fid.Path != "" { + fileName = safepath.Join(fid.Path, fileName) + } + if r.path != "" { + fileName = safepath.Join(r.path, fileName) + } + + err = r.target.Write(ctx, fileName, r.ref, body, commitMessage) + if err != nil { + result.Error = fmt.Errorf("failed to write file: %w", err) + } + + return result +} diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go new file mode 100644 index 00000000000..7092fcc0b5b --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -0,0 +1,124 @@ +package export + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" +) + +type ExportWorker struct { + // Tempdir for repo clones + clonedir string + + // required to create clients + clientFactory *resources.ClientFactory + + // Check where values are currently saved + storageStatus dualwrite.Service + + // Decrypt secrets in config + secrets secrets.Service +} + +func NewExportWorker(clientFactory *resources.ClientFactory, + storageStatus dualwrite.Service, + secrets secrets.Service, + clonedir string, +) *ExportWorker { + return &ExportWorker{ + clonedir, + clientFactory, + storageStatus, + secrets, + } +} + +func (r *ExportWorker) IsSupported(ctx context.Context, job provisioning.Job) bool { + return job.Spec.Action == provisioning.JobActionExport +} + +// Process will start a job +func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error { + options := job.Spec.Push + if options == nil { + return errors.New("missing export settings") + } + + // Can write to external branch + err := repository.IsWriteAllowed(repo.Config(), options.Branch) + if err != nil { + return err + } + + // Use the existing clone if already checked out + buffered, ok := repo.(*gogit.GoGitRepo) + if !ok && repo.Config().Spec.GitHub != nil { + progress.SetMessage(ctx, "clone target") + buffered, err = gogit.Clone(ctx, repo.Config(), gogit.GoGitCloneOptions{ + Root: r.clonedir, + SingleCommitBeforePush: true, + // TODO: make this configurable + Timeout: 10 * time.Minute, + }, r.secrets, os.Stdout) + if err != nil { + return fmt.Errorf("unable to clone target: %w", err) + } + + repo = buffered // send all writes to the buffered repo + defer func() { + if err := buffered.Remove(ctx); err != nil { + logging.FromContext(ctx).Error("failed to remove cloned repository after export", "err", err) + } + }() + + options.Branch = "" // :( the branch is now baked into the repo + } + + rw, ok := repo.(repository.ReaderWriter) + if !ok { + return errors.New("export job submitted targeting repository that is not a ReaderWriter") + } + + clients, err := r.clientFactory.Clients(ctx, repo.Config().Namespace) + if err != nil { + return err + } + + worker := newExportJob(ctx, rw, *options, clients, progress) + + // Load and write all folders + progress.SetMessage(ctx, "start folder export") + err = worker.loadFolders(ctx) + if err != nil { + return err + } + + progress.SetMessage(ctx, "start resource export") + err = worker.loadResources(ctx) + if err != nil { + return err + } + + if buffered != nil { + progress.SetMessage(ctx, "push changes") + if err := buffered.Push(ctx, gogit.GoGitPushOptions{ + // TODO: make this configurable + Timeout: 10 * time.Minute, + }, os.Stdout); err != nil { + return fmt.Errorf("error pushing changes: %w", err) + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/folders.go b/pkg/registry/apis/provisioning/jobs/migrate/folders.go new file mode 100644 index 00000000000..0f9090e4f8c --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/folders.go @@ -0,0 +1,113 @@ +package migrate + +import ( + "context" + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/storage/unified/parquet" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var _ resource.BulkResourceWriter = (*folderReader)(nil) + +type folderReader struct { + tree *resources.FolderTree + targetRepoName string +} + +// Close implements resource.BulkResourceWrite. +func (f *folderReader) Close() error { + return nil +} + +// CloseWithResults implements resource.BulkResourceWrite. +func (f *folderReader) CloseWithResults() (*resource.BulkResponse, error) { + return &resource.BulkResponse{}, nil +} + +// Write implements resource.BulkResourceWrite. +func (f *folderReader) Write(ctx context.Context, key *resource.ResourceKey, value []byte) error { + item := &unstructured.Unstructured{} + err := item.UnmarshalJSON(value) + if err != nil { + return fmt.Errorf("unmarshal unstructured to JSON: %w", err) + } + + return f.tree.AddUnstructured(item, f.targetRepoName) +} + +func (j *migrationJob) migrateLegacyFolders(ctx context.Context) error { + logger := j.logger + j.progress.SetMessage(ctx, "reading folder tree") + + repoName := j.target.Config().Name + + j.progress.SetMessage(ctx, "migrate folder tree from legacy") + reader := &folderReader{ + tree: j.folderTree, + targetRepoName: repoName, + } + _, err := j.legacy.Migrate(ctx, legacy.MigrateOptions{ + Namespace: j.namespace, + Resources: []schema.GroupResource{{ + Group: folders.GROUP, + Resource: folders.RESOURCE, + }}, + Store: parquet.NewBulkResourceWriterClient(reader), + }) + if err != nil { + return fmt.Errorf("unable to read folders from legacy storage %w", err) + } + + // create folders first is required so that empty folders exist when finished + j.progress.SetMessage(ctx, "write folders") + + err = j.folderTree.Walk(ctx, func(ctx context.Context, folder resources.Folder) error { + p := folder.Path + logger = logger.With("path", p) + + result := jobs.JobResourceResult{ + Name: folder.ID, + Resource: folders.RESOURCE, + Group: folders.GROUP, + Path: p, + } + + _, err := j.target.Read(ctx, p, "") + if err != nil && !(errors.Is(err, repository.ErrFileNotFound) || apierrors.IsNotFound(err)) { + result.Error = fmt.Errorf("failed to check if folder exists before writing: %w", err) + return result.Error + } else if err == nil { + logger.Info("folder already exists") + result.Action = repository.FileActionIgnored + j.progress.Record(ctx, result) + return nil + } + + result.Action = repository.FileActionCreated + msg := fmt.Sprintf("export folder %s", p) + // Create with an empty body will make a folder (or .keep file if unsupported) + if err := j.target.Create(ctx, p, "", nil, msg); err != nil { + result.Error = fmt.Errorf("failed to write folder in repo: %w", err) + j.progress.Record(ctx, result) + return result.Error + } + + return nil + }) + if err != nil { + return fmt.Errorf("failed to write folders: %w", err) + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/import.go b/pkg/registry/apis/provisioning/jobs/migrate/import.go new file mode 100644 index 00000000000..3a4fa42a180 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/import.go @@ -0,0 +1,90 @@ +package migrate + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc/metadata" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +// called when an error exists +func stopReadingUnifiedStorage(ctx context.Context, dual dualwrite.Service) error { + kinds := []schema.GroupResource{{ + Group: folders.GROUP, + Resource: folders.RESOURCE, + }, { + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + }} + + for _, gr := range kinds { + status, _ := dual.Status(ctx, gr) + status.ReadUnified = false + status.Migrated = 0 + status.Migrating = 0 + _, err := dual.Update(ctx, status) + if err != nil { + return err + } + } + return nil +} + +func (j *migrationJob) wipeUnifiedAndSetMigratedFlag(ctx context.Context, dual dualwrite.Service) error { + kinds := []schema.GroupResource{{ + Group: folders.GROUP, + Resource: folders.RESOURCE, + }, { + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + }} + + for _, gr := range kinds { + status, _ := dual.Status(ctx, gr) + if status.ReadUnified { + return fmt.Errorf("unexpected state - already using unified storage for: %s", gr) + } + if status.Migrating > 0 { + if time.Since(time.UnixMilli(status.Migrating)) < time.Second*30 { + return fmt.Errorf("another migration job is running for: %s", gr) + } + } + settings := resource.BulkSettings{ + RebuildCollection: true, // wipes everything in the collection + Collection: []*resource.ResourceKey{{ + Namespace: j.namespace, + Group: gr.Group, + Resource: gr.Resource, + }}, + } + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + stream, err := j.batch.BulkProcess(ctx) + if err != nil { + return fmt.Errorf("error clearing unified %s / %w", gr, err) + } + stats, err := stream.CloseAndRecv() + if err != nil { + return fmt.Errorf("error clearing unified %s / %w", gr, err) + } + logger := logging.FromContext(ctx) + logger.Error("cleared unified stoage", "stats", stats) + + status.Migrated = time.Now().UnixMilli() // but not really... since the sync is starting + status.ReadUnified = true + status.WriteLegacy = false // keep legacy "clean" + _, err = dual.Update(ctx, status) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/resources.go b/pkg/registry/apis/provisioning/jobs/migrate/resources.go new file mode 100644 index 00000000000..320fb7bc5cb --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/resources.go @@ -0,0 +1,234 @@ +package migrate + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/storage/unified/parquet" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var _ resource.BulkResourceWriter = (*resourceReader)(nil) + +type resourceReader struct { + job *migrationJob +} + +// Close implements resource.BulkResourceWriter. +func (r *resourceReader) Close() error { + return nil +} + +// CloseWithResults implements resource.BulkResourceWriter. +func (r *resourceReader) CloseWithResults() (*resource.BulkResponse, error) { + return &resource.BulkResponse{}, nil +} + +// Write implements resource.BulkResourceWriter. +func (r *resourceReader) Write(ctx context.Context, key *resource.ResourceKey, value []byte) error { + // Reuse the same parse+cleanup logic + parsed, err := r.job.parser.Parse(ctx, &repository.FileInfo{ + Path: "", // empty path to ignore file system + Data: value, + }, false) + if err != nil { + // TODO: should we fail the entire execution? + return fmt.Errorf("failed to unmarshal unstructured: %w", err) + } + + // clear anything so it will get written + parsed.Meta.SetManagerProperties(utils.ManagerProperties{}) + parsed.Meta.SetSourceProperties(utils.SourceProperties{}) + + if result := r.job.write(ctx, parsed.Obj); result.Error != nil { + r.job.progress.Record(ctx, result) + if err := r.job.progress.TooManyErrors(); err != nil { + return err + } + } + + return nil +} + +func (j *migrationJob) migrateLegacyResources(ctx context.Context) error { + kinds := []schema.GroupVersionResource{{ + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + Version: "v1alpha1", + }} + + for _, kind := range kinds { + j.progress.SetMessage(ctx, fmt.Sprintf("migrate %s resource", kind.Resource)) + gr := kind.GroupResource() + opts := legacy.MigrateOptions{ + Namespace: j.namespace, + WithHistory: j.options.History, + Resources: []schema.GroupResource{gr}, + Store: parquet.NewBulkResourceWriterClient(&resourceReader{job: j}), + OnlyCount: true, // first get the count + } + stats, err := j.legacy.Migrate(ctx, opts) + if err != nil { + return fmt.Errorf("unable to count legacy items %w", err) + } + + // FIXME: explain why we calculate it in this way + if len(stats.Summary) > 0 { + count := stats.Summary[0].Count // + history := stats.Summary[0].History + if history > count { + count = history // the number of items we will process + } + j.progress.SetTotal(ctx, int(count)) + } + + opts.OnlyCount = false // this time actually write + _, err = j.legacy.Migrate(ctx, opts) + if err != nil { + return fmt.Errorf("error running legacy migrate %s %w", kind.Resource, err) + } + } + return nil +} + +func (j *migrationJob) write(ctx context.Context, obj *unstructured.Unstructured) jobs.JobResourceResult { + gvk := obj.GroupVersionKind() + result := jobs.JobResourceResult{ + Name: obj.GetName(), + Resource: gvk.Kind, + Group: gvk.Group, + Action: repository.FileActionCreated, + } + + if err := ctx.Err(); err != nil { + result.Error = fmt.Errorf("context error: %w", err) + return result + } + + meta, err := utils.MetaAccessor(obj) + if err != nil { + result.Error = fmt.Errorf("extract meta accessor: %w", err) + return result + } + + // Message from annotations + commitMessage := meta.GetMessage() + if commitMessage == "" { + g := meta.GetGeneration() + if g > 0 { + commitMessage = fmt.Sprintf("Generation: %d", g) + } else { + commitMessage = "exported from grafana" + } + } + + name := meta.GetName() + manager, _ := meta.GetManagerProperties() + if manager.Identity == j.target.Config().GetName() { + result.Action = repository.FileActionIgnored + return result + } + + title := meta.FindTitle("") + if title == "" { + title = name + } + folder := meta.GetFolder() + + // Add the author in context (if available) + ctx = j.withAuthorSignature(ctx, meta) + + // Get the absolute path of the folder + fid, ok := j.folderTree.DirPath(folder, "") + if !ok { + // FIXME: Shouldn't this fail instead? + fid = resources.Folder{ + Path: "__folder_not_found/" + slugify.Slugify(folder), + } + j.logger.Error("folder of item was not in tree of repository") + } + + result.Path = fid.Path + + // Clear the metadata + delete(obj.Object, "metadata") + + if j.options.Identifier { + meta.SetName(name) // keep the identifier in the metadata + } + + body, err := json.MarshalIndent(obj.Object, "", " ") + if err != nil { + result.Error = fmt.Errorf("failed to marshal dashboard: %w", err) + return result + } + + fileName := slugify.Slugify(title) + ".json" + if fid.Path != "" { + fileName = safepath.Join(fid.Path, fileName) + } + + err = j.target.Write(ctx, fileName, "", body, commitMessage) + if err != nil { + result.Error = fmt.Errorf("failed to write file: %w", err) + } + + return result +} + +func removeUnprovisioned(ctx context.Context, client dynamic.ResourceInterface, progress jobs.JobProgressRecorder) error { + rawList, err := client.List(ctx, metav1.ListOptions{Limit: 10000}) + if err != nil { + return fmt.Errorf("failed to list resources: %w", err) + } + + if rawList.GetContinue() != "" { + return fmt.Errorf("unable to list all resources in one request: %s", rawList.GetContinue()) + } + + for _, item := range rawList.Items { + // Create a pointer to the item since MetaAccessor requires a pointer + itemPtr := &item + meta, err := utils.MetaAccessor(itemPtr) + if err != nil { + return fmt.Errorf("extract meta accessor: %w", err) + } + + // Skip if managed + _, ok := meta.GetManagerProperties() + if ok { + continue + } + + result := jobs.JobResourceResult{ + Name: item.GetName(), + Resource: item.GetKind(), + Group: item.GroupVersionKind().Group, + Action: repository.FileActionDeleted, + } + + if err = client.Delete(ctx, item.GetName(), metav1.DeleteOptions{}); err != nil { + result.Error = fmt.Errorf("failed to delete folder: %w", err) + progress.Record(ctx, result) + return result.Error + } + + progress.Record(ctx, result) + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/users.go b/pkg/registry/apis/provisioning/jobs/migrate/users.go new file mode 100644 index 00000000000..30d52faf770 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/users.go @@ -0,0 +1,53 @@ +package migrate + +import ( + "context" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +func (j *migrationJob) loadUsers(ctx context.Context) error { + client, err := j.parser.Clients().User() + if err != nil { + return err + } + + rawList, err := client.List(ctx, metav1.ListOptions{Limit: 10000}) + if err != nil { + return fmt.Errorf("failed to list users: %w", err) + } + if rawList.GetContinue() != "" { + return fmt.Errorf("unable to list all users in one request: %s", rawList.GetContinue()) + } + + var ok bool + j.userInfo = make(map[string]repository.CommitSignature) + for _, item := range rawList.Items { + sig := repository.CommitSignature{} + // FIXME: should we improve logging here? + sig.Name, ok, err = unstructured.NestedString(item.Object, "spec", "login") + if !ok || err != nil { + continue + } + sig.Email, ok, err = unstructured.NestedString(item.Object, "spec", "email") + if !ok || err != nil { + continue + } + + if sig.Name == sig.Email { + if sig.Name == "" { + sig.Name = item.GetName() + } else if strings.Contains(sig.Email, "@") { + sig.Email = "" // don't use the same value for name+email + } + } + + j.userInfo["user:"+item.GetName()] = sig + } + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker.go b/pkg/registry/apis/provisioning/jobs/migrate/worker.go new file mode 100644 index 00000000000..1a66066b2db --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker.go @@ -0,0 +1,333 @@ +package migrate + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/utils" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type MigrationWorker struct { + // Tempdir for repo clones + clonedir string + + // temporary... while we still do an import + parsers *resources.ParserFactory + + // Check where values are currently saved + storageStatus dualwrite.Service + + // Support reading from history + legacyMigrator legacy.LegacyMigrator + + // Direct access to unified storage... use carefully! + bulk resource.BulkStoreClient + + // Decrypt secret from config object + secrets secrets.Service + + // Delegate the export to the export worker + exportWorker *export.ExportWorker + + // Delegate the import to sync worker + syncWorker *sync.SyncWorker +} + +func NewMigrationWorker( + legacyMigrator legacy.LegacyMigrator, + parsers *resources.ParserFactory, // should not be necessary! + storageStatus dualwrite.Service, + batch resource.BulkStoreClient, + secrets secrets.Service, + exportWorker *export.ExportWorker, + syncWorker *sync.SyncWorker, + clonedir string, +) *MigrationWorker { + return &MigrationWorker{ + clonedir, + parsers, + storageStatus, + legacyMigrator, + batch, + secrets, + exportWorker, + syncWorker, + } +} + +func (w *MigrationWorker) IsSupported(ctx context.Context, job provisioning.Job) bool { + return job.Spec.Action == provisioning.JobActionMigrate +} + +// Process will start a job +func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error { + options := job.Spec.Migrate + if options == nil { + return errors.New("missing migrate settings") + } + + var ( + err error + buffered *gogit.GoGitRepo + ) + + isFromLegacy := dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, w.storageStatus) + progress.SetTotal(ctx, 10) // will show a progress bar + + // TODO: we should fail fast if migration is not possible and not always clone the repository. + if repo.Config().Spec.GitHub != nil { + progress.SetMessage(ctx, "clone "+repo.Config().Spec.GitHub.URL) + reader, writer := io.Pipe() + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + progress.SetMessage(ctx, scanner.Text()) + } + }() + + buffered, err = gogit.Clone(ctx, repo.Config(), gogit.GoGitCloneOptions{ + Root: w.clonedir, + SingleCommitBeforePush: !(options.History && isFromLegacy), + // TODO: make this configurable + Timeout: 10 * time.Minute, + }, w.secrets, writer) + if err != nil { + return fmt.Errorf("unable to clone target: %w", err) + } + + repo = buffered // send all writes to the buffered repo + defer func() { + if err := buffered.Remove(ctx); err != nil { + logging.FromContext(ctx).Error("failed to remove cloned repository after migrate", "err", err) + } + }() + } + + rw, ok := repo.(repository.ReaderWriter) + if !ok { + return errors.New("migration job submitted targeting repository that is not a ReaderWriter") + } + + if isFromLegacy { + return w.migrateFromLegacy(ctx, rw, buffered, *options, progress) + } + + return w.migrateFromUnifiedStorage(ctx, rw, *options, progress) +} + +// migrateFromLegacy will export the resources from legacy storage and import them into the target repository +func (w *MigrationWorker) migrateFromLegacy(ctx context.Context, rw repository.ReaderWriter, buffered *gogit.GoGitRepo, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { + parser, err := w.parsers.GetParser(ctx, rw) + if err != nil { + return fmt.Errorf("error getting parser: %w", err) + } + + worker, err := newMigrationJob(ctx, rw, options, parser, w.bulk, w.legacyMigrator, progress) + if err != nil { + return fmt.Errorf("error creating job: %w", err) + } + + if options.History { + progress.SetMessage(ctx, "loading users") + err = worker.loadUsers(ctx) + if err != nil { + return fmt.Errorf("error loading users: %w", err) + } + } + + progress.SetMessage(ctx, "exporting legacy folders") + err = worker.migrateLegacyFolders(ctx) + if err != nil { + return err + } + + progress.SetMessage(ctx, "exporting legacy resources") + err = worker.migrateLegacyResources(ctx) + if err != nil { + return err + } + + if buffered != nil { + progress.SetMessage(ctx, "pushing changes") + reader, writer := io.Pipe() + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + progress.SetMessage(ctx, scanner.Text()) + } + }() + + if err := buffered.Push(ctx, gogit.GoGitPushOptions{ + // TODO: make this configurable + Timeout: 10 * time.Minute, + }, writer); err != nil { + return fmt.Errorf("error pushing changes: %w", err) + } + } + + progress.SetMessage(ctx, "resetting unified storage") + if err = worker.wipeUnifiedAndSetMigratedFlag(ctx, w.storageStatus); err != nil { + return fmt.Errorf("unable to reset unified storage %w", err) + } + + // Reset the results after the export as pull will operate on the same resources + progress.ResetResults() + + // Delegate the import to a sync (from the already checked out go-git repository!) + progress.SetMessage(ctx, "pulling resources") + err = w.syncWorker.Process(ctx, rw, provisioning.Job{ + Spec: provisioning.JobSpec{ + Pull: &provisioning.SyncJobOptions{ + Incremental: false, + }, + }, + }, progress) + if err != nil { // this will have an error when too many errors exist + progress.SetMessage(ctx, "error importing resources, reverting") + if e2 := stopReadingUnifiedStorage(ctx, w.storageStatus); e2 != nil { + logger := logging.FromContext(ctx) + logger.Warn("error trying to revert dual write settings after an error", "err", err) + } + } + + return err +} + +// migrateFromUnifiedStorage will export the resources from unified storage and import them into the target repository +func (w *MigrationWorker) migrateFromUnifiedStorage(ctx context.Context, repo repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { + parser, err := w.parsers.GetParser(ctx, repo) + if err != nil { + return fmt.Errorf("error getting parser: %w", err) + } + + progress.SetMessage(ctx, "exporting unified storage resources") + if err := w.exportWorker.Process(ctx, repo, provisioning.Job{ + Spec: provisioning.JobSpec{ + Push: &provisioning.ExportJobOptions{ + Identifier: options.Identifier, + }, + }, + }, progress); err != nil { + return fmt.Errorf("export resources: %w", err) + } + + // Reset the results after the export as pull will operate on the same resources + progress.ResetResults() + + progress.SetMessage(ctx, "pulling resources") + err = w.syncWorker.Process(ctx, repo, provisioning.Job{ + Spec: provisioning.JobSpec{ + Pull: &provisioning.SyncJobOptions{ + Incremental: false, + }, + }, + }, progress) + if err != nil { + return fmt.Errorf("pull resources: %w", err) + } + + folderClient, err := parser.Clients().Folder() + if err != nil { + return fmt.Errorf("unable to get folder client: %w", err) + } + + dashboardClient, err := parser.Clients().Dashboard() + if err != nil { + return fmt.Errorf("unable to get dashboard client: %w", err) + } + + progress.SetMessage(ctx, "removing unprovisioned folders") + err = removeUnprovisioned(ctx, folderClient, progress) + if err != nil { + return fmt.Errorf("remove unprovisioned folders: %w", err) + } + + progress.SetMessage(ctx, "removing unprovisioned dashboards") + err = removeUnprovisioned(ctx, dashboardClient, progress) + if err != nil { + return fmt.Errorf("remove unprovisioned dashboards: %w", err) + } + + return nil +} + +// MigrationJob holds all context for a running job +type migrationJob struct { + logger logging.Logger + target repository.ReaderWriter + legacy legacy.LegacyMigrator + parser *resources.Parser + batch resource.BulkStoreClient + + namespace string + + progress jobs.JobProgressRecorder + + userInfo map[string]repository.CommitSignature + folderTree *resources.FolderTree + + options provisioning.MigrateJobOptions +} + +func newMigrationJob(ctx context.Context, + target repository.ReaderWriter, + options provisioning.MigrateJobOptions, + parser *resources.Parser, + batch resource.BulkStoreClient, + legacyMigrator legacy.LegacyMigrator, + progress jobs.JobProgressRecorder, +) (*migrationJob, error) { + return &migrationJob{ + namespace: target.Config().Namespace, + target: target, + logger: logging.FromContext(ctx), + progress: progress, + options: options, + parser: parser, + batch: batch, + legacy: legacyMigrator, + folderTree: resources.NewEmptyFolderTree(), + }, nil +} + +func (j *migrationJob) withAuthorSignature(ctx context.Context, item utils.GrafanaMetaAccessor) context.Context { + if j.userInfo == nil { + return ctx + } + id := item.GetUpdatedBy() + if id == "" { + id = item.GetCreatedBy() + } + if id == "" { + id = "grafana" + } + + sig := j.userInfo[id] // lookup + if sig.Name == "" && sig.Email == "" { + sig.Name = id + } + t, err := item.GetUpdatedTimestamp() + if err == nil && t != nil { + sig.When = *t + } else { + sig.When = item.GetCreationTimestamp().Time + } + + return repository.WithAuthorSignature(ctx, sig) +} diff --git a/pkg/registry/apis/provisioning/jobs/persistentstore.go b/pkg/registry/apis/provisioning/jobs/persistentstore.go new file mode 100644 index 00000000000..c8e836efe41 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/persistentstore.go @@ -0,0 +1,427 @@ +package jobs + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/apifmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +const ( + // LabelJobClaim includes the timestamp when the job was claimed. + // The label must be formatted as milliseconds from Epoch. This grants a natural ordering, allowing for less-than operators in label selectors. + // The natural ordering would be broken if the number rolls over into 1 more digit. This won't happen before Nov, 2286. + LabelJobClaim = "provisioning.grafana.app/claim" + // LabelJobOriginalName contains the Job's name as a label. This allows for label selectors to find the archived version of a job. + LabelJobOriginalName = "provisioning.grafana.app/original-name" + // LabelRepository contains the repository name as a label. This allows for label selectors to find the archived version of a job. + LabelRepository = "provisioning.grafana.app/repository" +) + +var ( + ErrNoJobs = &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Reason: metav1.StatusReasonConflict, + Message: "no jobs are available to claim, try again later", + Code: http.StatusNoContent, + Details: &metav1.StatusDetails{ + Group: provisioning.GROUP, + Kind: provisioning.JobResourceInfo.GetName(), + RetryAfterSeconds: 3, + }, + }, + } + + errWouldCreate = errors.New("this call would have created a new resource; it is rejected") + failCreation rest.ValidateObjectFunc = func(_ context.Context, _ runtime.Object) error { + return errWouldCreate + } +) + +type Queue interface { + // Insert adds a new job to the queue. + // + // This saves it if it is a new job, or fails with ErrJobAlreadyExists if one with the same name already exists. + Insert(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) +} + +var _ Queue = (*persistentStore)(nil) + +type jobStorage interface { + rest.Creater + rest.Lister + rest.Patcher + rest.GracefulDeleter +} + +// persistentStore is a job queue abstraction. +// It calls out to a real storage implementation to store the jobs, and a separate storage for historic jobs that have been completed. +// When persistentStore claims a job, it will update the status of it. This does a ResourceVersion check to ensure it is atomic; if the job has been claimed by another worker, the claim will fail. +// When a job is completed, it is moved to the historic job store by first deleting it from the job store and then creating it in the historic job store. We are fine with the job being lost if the historic job store fails to create it. +type persistentStore struct { + jobStore jobStorage + historicJobStore rest.Creater + + // clock is a function that returns the current time. + clock func() time.Time + // expiry is the time after which a job is considered abandoned. + // If a job is abandoned, it will have its claim cleaned up periodically. + expiry time.Duration + + // notifications has a signal sent to it when a new job is inserted. If a value already exists, nothing is sent. + // + // This is very similar to the concept of a Waker in Rust: + notifications chan struct{} +} + +func NewStore( + jobStore jobStorage, + historicJobStore rest.Creater, + expiry time.Duration, +) (*persistentStore, error) { + if expiry <= 0 { + expiry = time.Second * 30 + } + + return &persistentStore{ + jobStore: jobStore, + historicJobStore: historicJobStore, + + clock: time.Now, + expiry: expiry, + + notifications: make(chan struct{}, 1), + }, nil +} + +// Claim takes a job from storage, marks it as ours, and returns it. +// +// Any job which has not been claimed by another worker is fair game. +// +// If err is not nil, the job and rollback values are always nil. +// The err may be ErrNoJobs if there are no jobs to claim. +func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rollback func(), err error) { + requirement, err := labels.NewRequirement(LabelJobClaim, selection.DoesNotExist, nil) + if err != nil { + return nil, nil, apifmt.Errorf("could not create requirement: %w", err) + } + + jobsObj, err := s.jobStore.List(ctx, &internalversion.ListOptions{ + LabelSelector: labels.NewSelector().Add(*requirement), + Limit: 16, + }) + if err != nil { + return nil, nil, apifmt.Errorf("failed to list jobs: %w", err) + } + jobs, ok := jobsObj.(*provisioning.JobList) + if !ok { + return nil, nil, apifmt.Errorf("unexpected object type %T", jobsObj) + } + + if len(jobs.Items) == 0 { + return nil, nil, ErrNoJobs + } + + for _, job := range jobs.Items { + if job.Labels == nil { + job.Labels = make(map[string]string) + } + job.Labels[LabelJobClaim] = strconv.FormatInt(s.clock().UnixMilli(), 10) + + // We list jobs from all namespaces. So when we want to update a specific job, we also need its namespace in the context. + ctx := request.WithNamespace(ctx, job.GetNamespace()) + // Likewise, we should use the provisioning identity now that we have the namespace we are operating within. + ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) + if err != nil { + // This should never happen, as it is already a valid namespace from the job existing... but better be safe. + return nil, nil, apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) + } + + // This relies on the resource version being updated for us. + // If the resource version we pass in via the current job is not the same as the one currently in the store, it will fail with Conflict. + // This is the desired behavior, as it ensures that claims are atomic. + updated, _, err := s.jobStore.Update(ctx, + job.GetName(), // name + rest.DefaultUpdatedObjectInfo(&job), // objInfo + failCreation, // createValidation + nil, // updateValidation + false, // forceAllowCreate + &metav1.UpdateOptions{}, // options + ) + if apierrors.IsConflict(err) || errors.Is(err, errWouldCreate) { + // On conflict: another worker claimed the job before us. + // On would create: the job was completed and deleted before we could claim it. + // We'll just move on to the next job. + continue + } + if err != nil { + return nil, nil, apifmt.Errorf("failed to claim job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + updatedJob, ok := updated.(*provisioning.Job) + if !ok { + return nil, nil, apifmt.Errorf("unexpected object type %T", updated) + } + + return updatedJob.DeepCopy(), func() { + // Rolling back does not need to care about the parent's cancellation state. + // This will also use the parent context (i.e. from the for loop!), ensuring we have permissions to do this. + ctx = context.WithoutCancel(ctx) + + logger := logging.FromContext(ctx).With("namespace", updatedJob.GetNamespace(), "job", updatedJob.GetName()) + + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + refetched, err := s.jobStore.Get(timeoutCtx, updatedJob.GetName(), &metav1.GetOptions{}) + cancel() // we have no response body to read (the obj already contains all of it), so just cancel immediately + if apierrors.IsNotFound(err) { + // The job was probably completed already. Nothing to roll back! + return + } else if err != nil { + // We failed. Nothing much we can do but let the job be cleaned up by the periodic cleaner. + logger.Warn("failed to roll back job claim; letting periodic cleaner deal with it", "error", err) + return + } + refetchedJob, ok := refetched.(*provisioning.Job) + if !ok { + logger.Warn("failed to roll back job claim: the job we got is not a *provisioning.Job?", "got", refetched) + return + } + + // Rollback the claim. + delete(refetchedJob.Labels, LabelJobClaim) + refetchedJob.Status.State = provisioning.JobStatePending + + timeoutCtx, cancel = context.WithTimeout(ctx, 5*time.Second) + _, _, err = s.jobStore.Update(timeoutCtx, + refetchedJob.GetName(), // name + rest.DefaultUpdatedObjectInfo(refetchedJob), // objInfo + failCreation, // createValidation + nil, // updateValidation + false, // forceAllowCreate + &metav1.UpdateOptions{}, // options + ) + cancel() // we have no response body to read (the obj already contains all of it), so just cancel immediately + if err != nil && !apierrors.IsConflict(err) && !errors.Is(err, errWouldCreate) { + logger.Warn("failed to roll back job claim; letting periodic cleaner deal with it", "error", err) + } else if err != nil { + logger.Debug("failed to roll back job claim; got an OK error", "error", err) + } + }, nil + } + + // We failed to claim any jobs. + return nil, nil, ErrNoJobs +} + +// Update saves the job back to the store. +func (s *persistentStore) Update(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) { + obj, _, err := s.jobStore.Update(ctx, + job.GetName(), // name + rest.DefaultUpdatedObjectInfo(job), // objInfo + failCreation, // createValidation + nil, // updateValidation + false, // forceAllowCreate + &metav1.UpdateOptions{}, // options + ) + if err != nil { + return nil, apifmt.Errorf("failed to update job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + + updatedJob, ok := obj.(*provisioning.Job) + if !ok { + return nil, apifmt.Errorf("unexpected object type %T", obj) + } + + return updatedJob, nil +} + +// Complete marks a job as completed and moves it to the historic job store. +// When in the historic store, there is no more claim on the job. +func (s *persistentStore) Complete(ctx context.Context, job *provisioning.Job) error { + logger := logging.FromContext(ctx).With("namespace", job.GetNamespace(), "job", job.GetName()) + + // We need to delete the job from the job store and create it in the historic job store. + // We are fine with the job being lost if the historic job store fails to create it. + // + // We will assume that the caller is the claimant. If this is not true, an error is returned. + // This is a best-effort operation; if the job is not in the claimed state, we will still attempt to move it to the historic job store. + _, _, err := s.jobStore.Delete(ctx, job.GetName(), nil, &metav1.DeleteOptions{}) + if err != nil { + return apifmt.Errorf("failed to delete job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + logger.Debug("deleted job from job store") + + // We need to remove the claim label before moving the job to the historic job store. + if job.Labels == nil { + job.Labels = make(map[string]string) + } + delete(job.Labels, LabelJobClaim) + // We also need a new, unique name. + job.Labels[LabelJobOriginalName] = job.GetName() + job.Labels[LabelRepository] = job.Spec.Repository + job.GenerateName = job.Name + "-" + job.Name = "" + // We also reset the UID as this is not the same object. + job.ObjectMeta.UID = "" + // We aren't allowed to write with ResourceVersion set. + job.ResourceVersion = "" + + historicJob := &provisioning.HistoricJob{ + ObjectMeta: job.ObjectMeta, + Spec: job.Spec, + Status: job.Status, + } + _, err = s.historicJobStore.Create(ctx, historicJob, nil, &metav1.CreateOptions{}) + if err != nil { + // We're not going to return this as it is not critical. Not ideal, but not critical. + logger.Warn("failed to create historic job", "historic_job", *historicJob, "error", err) + } else { + logger.Debug("created historic job", "historic_job", *historicJob) + } + + logger.Debug("job completion done") + return nil +} + +// Cleanup should be called periodically to clean up abandoned jobs. +// An abandoned job is one that has been claimed by a worker, but the worker has not updated the job in a while. +func (s *persistentStore) Cleanup(ctx context.Context) error { + if err := s.cleanupClaims(ctx); err != nil { + return apifmt.Errorf("failed to clean up claims: %w", err) + } + + return nil +} + +// cleanupClaims will clean up abandoned claims. +// Any claim that is older than the expiry time will have their claims removed. +// +// This is only necessary because Kubernetes does not support logical OR in label selectors. +func (s *persistentStore) cleanupClaims(ctx context.Context) error { + // We will list all jobs that have been claimed but not updated in a while. + // We will then remove the claim from them. + // We will not care about the result of the update, as the job may have been completed in the meantime. + expiry := s.clock().Add(-s.expiry).UnixMilli() + requirement, err := labels.NewRequirement(LabelJobClaim, selection.LessThan, []string{strconv.FormatInt(expiry, 10)}) + if err != nil { + return apifmt.Errorf("could not create requirement: %w", err) + } + + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + jobsObj, err := s.jobStore.List(timeoutCtx, &internalversion.ListOptions{ + LabelSelector: labels.NewSelector().Add(*requirement), + // We don't need to clean up everything all the time. Just do enough such that we have a fair amount of work. + Limit: 100, + }) + cancel() // by the time we have the list, there is no response body to read, so just cancel immediately + if err != nil { + return apifmt.Errorf("failed to list jobs: %w", err) + } + jobs, ok := jobsObj.(*provisioning.JobList) + if !ok { + return apifmt.Errorf("unexpected object type %T", jobsObj) + } + + for _, job := range jobs.Items { + if job.Labels == nil { + job.Labels = make(map[string]string) + } + delete(job.Labels, LabelJobClaim) + job.Status.State = provisioning.JobStatePending + + // We list jobs from all namespaces. So when we want to update a specific job, we also need its namespace in the context. + ctx := request.WithNamespace(ctx, job.GetNamespace()) + // Likewise, we should use the provisioning identity now that we have the namespace we are operating within. + ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) + if err != nil { + // This should never happen, as it is already a valid namespace from the job existing... but better be safe. + return apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) + } + + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + _, _, err := s.jobStore.Update(timeoutCtx, + job.GetName(), // name + rest.DefaultUpdatedObjectInfo(&job), // objInfo + failCreation, // createValidation + nil, // updateValidation + false, // forceAllowCreate + &metav1.UpdateOptions{}, // options + ) + cancel() // we have no response body to read, so just cancel immediately + if apierrors.IsConflict(err) || errors.Is(err, errWouldCreate) { + continue + } + if err != nil { + return apifmt.Errorf("failed to unclaim job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + } + + return nil +} + +// Insert adds a new job to the queue. +// +// The job name is not honoured. It will be overwritten with a name that fits the job. +// +// This saves it if it is a new job, or fails with `apierrors.IsAlreadyExists(err) == true` if one already exists. +func (s *persistentStore) Insert(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) { + s.generateJobName(job) // Side-effect: updates the job's name. + + obj, err := s.jobStore.Create(ctx, job, nil, &metav1.CreateOptions{}) + if apierrors.IsAlreadyExists(err) { + return nil, apifmt.Errorf("job '%s' in '%s' already exists: %w", job.GetName(), job.GetNamespace(), err) + } + if err != nil { + return nil, apifmt.Errorf("failed to create job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + + created, ok := obj.(*provisioning.Job) + if !ok { + return nil, apifmt.Errorf("unexpected object type %T", obj) + } + + select { + case s.notifications <- struct{}{}: + default: + // We don't want to block if there is already a notification waiting. + } + + return created, nil +} + +func (s *persistentStore) InsertNotifications() chan struct{} { + return s.notifications +} + +// generateJobName creates and updates the job's name to one that fits it. +func (s *persistentStore) generateJobName(job *provisioning.Job) { + switch job.Spec.Action { + case provisioning.JobActionMigrate, provisioning.JobActionSync: + // Sync and migrate jobs should never run at the same time. Hence, the name encapsulates them both (and the spec differentiates them). + job.Name = job.Spec.Repository + "-syncmigrate" + case provisioning.JobActionPullRequest: + var pr int + if job.Spec.PullRequest != nil { + pr = job.Spec.PullRequest.PR + } + // There may be multiple pull requests at the same time. They need different names. + job.Name = fmt.Sprintf("%s-pr-%d", job.Spec.Repository, pr) + default: + job.Name = fmt.Sprintf("%s-%s", job.Spec.Repository, job.Spec.Action) + } +} diff --git a/pkg/registry/apis/provisioning/jobs/progress.go b/pkg/registry/apis/provisioning/jobs/progress.go index 3a38fc902ce..949512ddb0a 100644 --- a/pkg/registry/apis/provisioning/jobs/progress.go +++ b/pkg/registry/apis/provisioning/jobs/progress.go @@ -36,22 +36,26 @@ type JobResourceResult struct { } type jobProgressRecorder struct { - started time.Time - total int - ref string - message string - resultCount int - errorCount int - errors []string - progressFn ProgressFn - summaries map[string]*provisioning.JobResourceSummary + started time.Time + total int + ref string + message string + finalMessage string + resultCount int + errorCount int + errors []string + notifyImmediatelyFn ProgressFn + maybeNotifyFn ProgressFn + summaries map[string]*provisioning.JobResourceSummary } func newJobProgressRecorder(ProgressFn ProgressFn) JobProgressRecorder { return &jobProgressRecorder{ - started: time.Now(), - progressFn: maybeNotifyProgress(5*time.Second, ProgressFn), - summaries: make(map[string]*provisioning.JobResourceSummary), + started: time.Now(), + // Have a faster notifier for messages and total + notifyImmediatelyFn: maybeNotifyProgress(500*time.Millisecond, ProgressFn), + maybeNotifyFn: maybeNotifyProgress(5*time.Second, ProgressFn), + summaries: make(map[string]*provisioning.JobResourceSummary), } } @@ -70,15 +74,26 @@ func (r *jobProgressRecorder) Record(ctx context.Context, result JobResourceResu } r.updateSummary(result) - r.notify(ctx) + r.maybeNotify(ctx) } -func (r *jobProgressRecorder) SetMessage(msg string) { +// ResetResults will reset the results of the job +func (r *jobProgressRecorder) ResetResults() { + r.resultCount = 0 + r.errorCount = 0 + r.errors = nil + r.summaries = make(map[string]*provisioning.JobResourceSummary) +} + +func (r *jobProgressRecorder) SetMessage(ctx context.Context, msg string) { r.message = msg + logging.FromContext(ctx).Info("job progress message", "message", msg) + r.notifyImmediately(ctx) } -func (r *jobProgressRecorder) GetMessage() string { - return r.message +func (r *jobProgressRecorder) SetFinalMessage(ctx context.Context, msg string) { + r.finalMessage = msg + logging.FromContext(ctx).Info("job final message", "message", msg) } func (r *jobProgressRecorder) SetRef(ref string) { @@ -89,8 +104,10 @@ func (r *jobProgressRecorder) GetRef() string { return r.ref } -func (r *jobProgressRecorder) SetTotal(total int) { +func (r *jobProgressRecorder) SetTotal(ctx context.Context, total int) { r.total = total + + r.notifyImmediately(ctx) } func (r *jobProgressRecorder) TooManyErrors() error { @@ -154,7 +171,7 @@ func (r *jobProgressRecorder) progress() float64 { return float64(r.resultCount) / float64(r.total) * 100 } -func (r *jobProgressRecorder) notify(ctx context.Context) { +func (r *jobProgressRecorder) notifyImmediately(ctx context.Context) { jobStatus := provisioning.JobStatus{ State: provisioning.JobStateWorking, Message: r.message, @@ -164,7 +181,22 @@ func (r *jobProgressRecorder) notify(ctx context.Context) { } logger := logging.FromContext(ctx) - if err := r.progressFn(ctx, jobStatus); err != nil { + if err := r.notifyImmediatelyFn(ctx, jobStatus); err != nil { + logger.Warn("error notifying immediate progress", "err", err) + } +} + +func (r *jobProgressRecorder) maybeNotify(ctx context.Context) { + jobStatus := provisioning.JobStatus{ + State: provisioning.JobStateWorking, + Message: r.message, + Errors: r.errors, + Progress: r.progress(), + Summary: r.summary(), + } + + logger := logging.FromContext(ctx) + if err := r.maybeNotifyFn(ctx, jobStatus); err != nil { logger.Warn("error notifying progress", "err", err) } } @@ -195,8 +227,8 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision } // Override message if progress have a more explicit message - if r.message != "" && jobStatus.State != provisioning.JobStateError { - jobStatus.Message = r.message + if r.finalMessage != "" && jobStatus.State != provisioning.JobStateError { + jobStatus.Message = r.finalMessage } return jobStatus diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go b/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go new file mode 100644 index 00000000000..0521d85d79f --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go @@ -0,0 +1,158 @@ +package pullrequest + +import ( + "bytes" + "context" + "fmt" + "html/template" + "net/url" + "path" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +// resourcePreview represents a resource that has changed in a pull request. +type resourcePreview struct { + Filename string + Path string + Action string + Kind string + OriginalURL string + PreviewURL string + PreviewScreenshotURL string +} + +const previewsCommentTemplate = `Hey there! 🎉 +Grafana spotted some changes for your resources in this pull request: +## Summary +| File Name | Kind | Path | Action | Links | +|-----------|------|------|--------|-------| +{{- range .}} +| {{.Filename}} | {{.Kind}} | {{.Path}} | {{.Action}} | {{- if .OriginalURL}}[Original]({{.OriginalURL}}){{- end}}{{- if and .OriginalURL .PreviewURL}}, {{end}}{{- if .PreviewURL}}[Preview]({{.PreviewURL}}){{- end}} | +{{- end}} + +Click the preview links above to view how your changes will look and compare them with the original and current versions. + +{{- range .}} +{{- if .PreviewScreenshotURL}} +### Preview of {{.Filename}} +![Preview]({{.PreviewScreenshotURL}}) +{{- end}}{{- end}}` + +// PreviewRenderer is an interface for rendering a preview of a file +type PreviewRenderer interface { + IsAvailable(ctx context.Context) bool + RenderDashboardPreview(ctx context.Context, namespace, repoName, path, ref string) (string, error) +} + +type Previewer struct { + template *template.Template + urlProvider func(namespace string) string + renderer PreviewRenderer +} + +func NewPreviewer(renderer PreviewRenderer, urlProvider func(namespace string) string) *Previewer { + return &Previewer{ + template: template.Must(template.New("comment").Parse(previewsCommentTemplate)), + urlProvider: urlProvider, + renderer: renderer, + } +} + +// GenerateComment creates a formatted comment for dashboard previews +func (p *Previewer) GenerateComment(previews []resourcePreview) (string, error) { + var buf bytes.Buffer + if err := p.template.Execute(&buf, previews); err != nil { + return "", fmt.Errorf("execute previews comment template: %w", err) + } + return buf.String(), nil +} + +// getOriginalURL returns the URL for the original version of the file based on the action +func (p *Previewer) getOriginalURL(ctx context.Context, f repository.VersionedFileChange, baseURL *url.URL, repoName, base, pullRequestURL string) string { + switch f.Action { + case repository.FileActionCreated: + return "" // No original URL for new files + case repository.FileActionUpdated: + return p.previewURL(baseURL, repoName, base, f.Path, pullRequestURL) + case repository.FileActionRenamed: + return p.previewURL(baseURL, repoName, base, f.PreviousPath, pullRequestURL) + case repository.FileActionDeleted: + return p.previewURL(baseURL, repoName, base, f.Path, pullRequestURL) + default: + logging.FromContext(ctx).Error("unknown file action for original URL", "action", f.Action) + return "" + } +} + +// getPreviewURL returns the URL for the preview version of the file based on the action +func (p *Previewer) getPreviewURL(ctx context.Context, f repository.VersionedFileChange, baseURL *url.URL, repoName, ref, pullRequestURL string) string { + switch f.Action { + case repository.FileActionCreated, repository.FileActionUpdated, repository.FileActionRenamed: + return p.previewURL(baseURL, repoName, ref, f.Path, pullRequestURL) + case repository.FileActionDeleted: + return "" // No preview URL for deleted files + default: + logging.FromContext(ctx).Error("unknown file action for preview URL", "action", f.Action) + return "" + } +} + +// previewURL returns the URL to preview the file in Grafana +func (p *Previewer) previewURL(u *url.URL, repoName, ref, filePath, pullRequestURL string) string { + baseURL := *u + baseURL = *baseURL.JoinPath("/admin/provisioning", repoName, "dashboard/preview", filePath) + + query := baseURL.Query() + if ref != "" { + query.Set("ref", ref) + } + if pullRequestURL != "" { + query.Set("pull_request_url", url.QueryEscape(pullRequestURL)) + } + baseURL.RawQuery = query.Encode() + + return baseURL.String() +} + +// Preview creates a preview for a single file change +func (p *Previewer) Preview( + ctx context.Context, + f repository.VersionedFileChange, + namespace string, + repoName string, + base string, + ref string, + pullRequestURL string, + generatePreview bool, +) (*resourcePreview, error) { + baseURL, err := url.Parse(p.urlProvider(namespace)) + if err != nil { + return nil, fmt.Errorf("error parsing base url: %w", err) + } + + preview := resourcePreview{ + Filename: path.Base(f.Path), + Path: f.Path, + Kind: "dashboard", // TODO: add more kinds + Action: string(f.Action), + OriginalURL: p.getOriginalURL(ctx, f, baseURL, repoName, base, pullRequestURL), + PreviewURL: p.getPreviewURL(ctx, f, baseURL, repoName, ref, pullRequestURL), + } + + if !generatePreview && len(preview.PreviewURL) == 0 { + logger.Info("skipping dashboard preview generation", "path", f.Path) + return &preview, nil + } + + screenshotURL, err := p.renderer.RenderDashboardPreview(ctx, namespace, repoName, f.Path, ref) + if err != nil { + return nil, fmt.Errorf("render dashboard preview: %w", err) + } + preview.PreviewScreenshotURL = screenshotURL + logger.Info("dashboard preview generated", "screenshotURL", screenshotURL) + + return &preview, nil +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/render.go b/pkg/registry/apis/provisioning/jobs/pullrequest/render.go new file mode 100644 index 00000000000..cf4a6b43275 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/render.go @@ -0,0 +1,93 @@ +package pullrequest + +import ( + "context" + "fmt" + "mime" + "os" + "path/filepath" + "strings" + "time" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type screenshotRenderer struct { + render rendering.Service + blobstore resource.BlobStoreClient + urlProvider func(namespace string) string + isPublic bool +} + +func NewScreenshotRenderer(render rendering.Service, blobstore resource.BlobStoreClient, isPublic bool, urlProvider func(namespace string) string) *screenshotRenderer { + return &screenshotRenderer{ + render: render, + blobstore: blobstore, + urlProvider: urlProvider, + isPublic: isPublic, + } +} + +func (r *screenshotRenderer) IsAvailable(ctx context.Context) bool { + return r.render != nil && r.render.IsAvailable(ctx) && r.blobstore != nil && r.isPublic +} + +func (r *screenshotRenderer) RenderDashboardPreview(ctx context.Context, namespace, repoName, path, ref string) (string, error) { + url := fmt.Sprintf("admin/provisioning/%s/dashboard/preview/%s?kiosk&ref=%s", repoName, path, ref) + + // TODO: why were we using a different context? + // renderContext := identity.WithRequester(context.Background(), r.id) + result, err := r.render.Render(ctx, rendering.RenderPNG, rendering.Opts{ + CommonOpts: rendering.CommonOpts{ + Path: url, + AuthOpts: rendering.AuthOpts{ + OrgID: 1, // TODO!!!, use the worker identity + UserID: 1, + OrgRole: identity.RoleAdmin, + }, + TimeoutOpts: rendering.TimeoutOpts{ + Timeout: time.Second * 30, + }, + }, + Theme: models.ThemeDark, // from config? + Width: 1024, + Height: -1, // full page height + }, nil) + if err != nil { + return "", err + } + + ext := filepath.Ext(result.FilePath) + body, err := os.ReadFile(result.FilePath) + if err != nil { + return "", err + } + + rsp, err := r.blobstore.PutBlob(ctx, &resource.PutBlobRequest{ + Resource: &resource.ResourceKey{ + Namespace: namespace, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GroupResource().Resource, + Name: repoName, + }, + Method: resource.PutBlobRequest_GRPC, + ContentType: mime.TypeByExtension(ext), // image/png + Value: body, + }) + if err != nil { + return "", err + } + if rsp.Url != "" { + return rsp.Url, nil + } + base := r.urlProvider(namespace) + if !strings.HasSuffix(base, "/") { + base += "/" + } + return fmt.Sprintf("%sapis/%s/namespaces/%s/repositories/%s/render/%s", + base, provisioning.APIVERSION, namespace, repoName, rsp.Uid), nil +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go new file mode 100644 index 00000000000..9ae20efad2e --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go @@ -0,0 +1,158 @@ +package pullrequest + +import ( + "context" + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +type PullRequestRepo interface { + Config() *provisioning.Repository + Read(ctx context.Context, path, ref string) (*repository.FileInfo, error) + CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) + ClearAllPullRequestFileComments(ctx context.Context, pr int) error + CommentPullRequestFile(ctx context.Context, pr int, path string, ref string, comment string) error + CommentPullRequest(ctx context.Context, pr int, comment string) error +} + +type PullRequestWorker struct { + parsers *resources.ParserFactory + previewer *Previewer +} + +func NewPullRequestWorker( + parsers *resources.ParserFactory, + previewer *Previewer, +) (*PullRequestWorker, error) { + return &PullRequestWorker{ + parsers: parsers, + previewer: previewer, + }, nil +} + +func (c *PullRequestWorker) IsSupported(ctx context.Context, job provisioning.Job) bool { + return job.Spec.Action == provisioning.JobActionPullRequest +} + +//nolint:gocyclo +func (c *PullRequestWorker) Process(ctx context.Context, + repo repository.Repository, + job provisioning.Job, + progress jobs.JobProgressRecorder, +) error { + cfg := repo.Config().Spec + options := job.Spec.PullRequest + if options == nil { + return apierrors.NewBadRequest("missing spec.pr") + } + + prRepo, ok := repo.(PullRequestRepo) + if !ok { + return fmt.Errorf("repository is not a github repository") + } + + reader, ok := repo.(repository.Reader) + if !ok { + return errors.New("pull request job submitted targeting repository that is not a Reader") + } + + parser, err := c.parsers.GetParser(ctx, reader) + if err != nil { + return fmt.Errorf("failed to get parser for %s: %w", repo.Config().Name, err) + } + + logger := logging.FromContext(ctx).With("pr", options.PR) + logger.Info("process pull request") + defer logger.Info("pull request processed") + + progress.SetMessage(ctx, "listing pull request files") + base := cfg.GitHub.Branch + ref := options.Hash + files, err := prRepo.CompareFiles(ctx, base, ref) + if err != nil { + return fmt.Errorf("failed to list pull request files: %s", err.Error()) + } + + progress.SetMessage(ctx, "clearing pull request comments") + if err := prRepo.ClearAllPullRequestFileComments(ctx, options.PR); err != nil { + return fmt.Errorf("failed to clear pull request comments: %+v", err) + } + + if len(files) == 0 { + progress.SetFinalMessage(ctx, "no files to process") + return nil + } + + progress.SetMessage(ctx, "processing pull request files") + previews := make([]resourcePreview, 0, len(files)) + for _, f := range files { + result := jobs.JobResourceResult{ + Path: f.Path, + } + + if err := resources.IsPathSupported(f.Path); err != nil { + result.Action = repository.FileActionIgnored + progress.Record(ctx, result) + continue + } + result.Action = f.Action + + fileInfo, err := prRepo.Read(ctx, f.Path, ref) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + + parsed, err := parser.Parse(ctx, fileInfo, true) + if err != nil { + if errors.Is(err, resources.ErrUnableToReadResourceBytes) { + logger.Debug("file is not a resource", "path", f.Path) + result.Action = repository.FileActionIgnored + progress.Record(ctx, result) + } else { + result.Error = fmt.Errorf("failed to parse resource: %w", err) + progress.Record(ctx, result) + } + continue + } + + result.Resource = parsed.GVR.Resource + result.Group = parsed.GVR.Group + result.Name = parsed.Obj.GetName() + + preview, err := c.previewer.Preview(ctx, f, job.Namespace, repo.Config().Name, cfg.GitHub.Branch, ref, options.URL, cfg.GitHub.GenerateDashboardPreviews) + if err != nil { + result.Error = fmt.Errorf("create preview: %w", err) + progress.Record(ctx, result) + continue + } + + previews = append(previews, *preview) + progress.Record(ctx, result) + } + + if len(previews) == 0 { + progress.SetFinalMessage(ctx, "no previews to add") + return nil + } + + progress.SetMessage(ctx, "generating previews comment") + comment, err := c.previewer.GenerateComment(previews) + if err != nil { + return fmt.Errorf("generate comment: %w", err) + } + + if err := prRepo.CommentPullRequest(ctx, options.PR, comment); err != nil { + return fmt.Errorf("comment pull request: %w", err) + } + logger.Info("previews comment added", "number", len(previews)) + + return nil +} diff --git a/pkg/registry/apis/provisioning/jobs/queue.go b/pkg/registry/apis/provisioning/jobs/queue.go index cb865e91578..4a0ab7e7e3e 100644 --- a/pkg/registry/apis/provisioning/jobs/queue.go +++ b/pkg/registry/apis/provisioning/jobs/queue.go @@ -11,35 +11,23 @@ type RepoGetter interface { GetRepository(ctx context.Context, name string) (repository.Repository, error) } -// Basic job queue infrastructure -type JobQueue interface { - // Add a new Job to the Queue. The status must be empty - Add(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) - - // Get the next job we should process - Next(ctx context.Context) *provisioning.Job - - // Update the status on a given job - // This is only valid if current job is not finished - Update(ctx context.Context, namespace string, name string, status provisioning.JobStatus) error - - // Register a worker (inline for now) - Register(worker Worker) -} - type JobProgressRecorder interface { Record(ctx context.Context, result JobResourceResult) - SetMessage(msg string) - GetMessage() string + ResetResults() + SetFinalMessage(ctx context.Context, msg string) + SetMessage(ctx context.Context, msg string) SetRef(ref string) GetRef() string - SetTotal(total int) + SetTotal(ctx context.Context, total int) TooManyErrors() error Complete(ctx context.Context, err error) provisioning.JobStatus } type Worker interface { IsSupported(ctx context.Context, job provisioning.Job) bool + // Process the job. The job status should be updated as the job progresses. + // + // The job spec and metadata MUST not be modified in the storage layer while this is running. All updates go via the progress type. Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress JobProgressRecorder) error } diff --git a/pkg/registry/apis/provisioning/jobs/store.go b/pkg/registry/apis/provisioning/jobs/store.go deleted file mode 100644 index 77e13c8c7d4..00000000000 --- a/pkg/registry/apis/provisioning/jobs/store.go +++ /dev/null @@ -1,367 +0,0 @@ -package jobs - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" - - "github.com/grafana/grafana-app-sdk/logging" - "github.com/grafana/grafana/pkg/apimachinery/identity" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/util" -) - -var ( - _ JobQueue = (*jobStore)(nil) - _ rest.Scoper = (*jobStore)(nil) - _ rest.SingularNameProvider = (*jobStore)(nil) - _ rest.Getter = (*jobStore)(nil) - _ rest.Lister = (*jobStore)(nil) - _ rest.Storage = (*jobStore)(nil) - _ rest.Watcher = (*jobStore)(nil) -) - -func NewJobStore(capacity int, getter RepoGetter) *jobStore { - return &jobStore{ - workers: make([]Worker, 0), - getter: getter, - rv: 1, - capacity: capacity, - jobs: []provisioning.Job{}, - watchSet: NewWatchSet(), - versioner: &storage.APIObjectVersioner{}, - } -} - -type jobStore struct { - getter RepoGetter - capacity int - workers []Worker - - // All jobs - jobs []provisioning.Job - rv int64 // updates whenever changed - watchSet *WatchSet - versioner storage.Versioner - - mutex sync.RWMutex -} - -// Implementing Kube interfaces - -func (s *jobStore) New() runtime.Object { - return provisioning.JobResourceInfo.NewFunc() -} - -func (s *jobStore) Destroy() {} - -func (s *jobStore) NamespaceScoped() bool { - return true // namespace == org -} - -func (s *jobStore) GetSingularName() string { - return provisioning.JobResourceInfo.GetSingularName() -} - -func (s *jobStore) NewList() runtime.Object { - return provisioning.JobResourceInfo.NewListFunc() -} - -func (s *jobStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return provisioning.JobResourceInfo.TableConverter().ConvertToTable(ctx, object, tableOptions) -} - -func (s *jobStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, ok := request.NamespaceFrom(ctx) - if !ok { - return nil, fmt.Errorf("missing namespace") - } - - queue := &provisioning.JobList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: strconv.FormatInt(s.rv, 10), - }, - } - - query := options.LabelSelector - - s.mutex.RLock() - defer s.mutex.RUnlock() - - for _, job := range s.jobs { - if job.Namespace != ns { - continue - } - - // maybe filter - if query != nil && !query.Matches(labels.Set(job.Labels)) { - continue - } - - copy := job.DeepCopy() - queue.Items = append(queue.Items, *copy) - } - - return queue, nil -} - -func (s *jobStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - s.mutex.RLock() - defer s.mutex.RUnlock() - - ns, ok := request.NamespaceFrom(ctx) - if !ok { - return nil, fmt.Errorf("missing namespace") - } - - for _, job := range s.jobs { - if job.Name == name && job.Namespace == ns { - return job.DeepCopy(), nil - } - } - - return nil, apierrors.NewNotFound(provisioning.JobResourceInfo.GroupResource(), name) -} - -func (s *jobStore) Watch(ctx context.Context, opts *internalversion.ListOptions) (watch.Interface, error) { - ns, ok := request.NamespaceFrom(ctx) - if !ok { - return nil, fmt.Errorf("missing namespace") - } - - p := storage.SelectionPredicate{ - Label: labels.Everything(), // TODO... limit - Field: fields.Everything(), - } - - // Can watch by label selection - jw := s.watchSet.newWatch(ctx, 0, p, s.versioner, &ns) - jw.Start() - return jw, nil -} - -// Implementing JobQueue - -// Register a worker (inline for now) -func (s *jobStore) Register(worker Worker) { - s.workers = append(s.workers, worker) -} - -func (s *jobStore) Add(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) { - if job.Namespace == "" { - return nil, apierrors.NewBadRequest("missing metadata.namespace") - } - if job.Name != "" { - return nil, apierrors.NewBadRequest("name will always be generated") - } - if job.Spec.Repository == "" { - return nil, apierrors.NewBadRequest("missing spec.repository") - } - if job.Spec.Action == "" { - return nil, apierrors.NewBadRequest("missing spec.action") - } - if job.Spec.Action == provisioning.JobActionExport && job.Spec.Push == nil { - return nil, apierrors.NewBadRequest("missing spec.push") - } - - if job.Spec.Action == provisioning.JobActionSync && job.Spec.Pull == nil { - return nil, apierrors.NewBadRequest("missing spec.pull") - } - - // Only for add - if job.Status.State != "" { - return nil, apierrors.NewBadRequest("must add jobs with empty status") - } - - if job.Labels == nil { - job.Labels = make(map[string]string) - } - job.Labels["repository"] = job.Spec.Repository // for now, make sure we can search Multi-tenant - job.Name = fmt.Sprintf("%s:%s:%s", job.Spec.Repository, job.Spec.Action, util.GenerateShortUID()) - - s.mutex.Lock() - defer s.mutex.Unlock() - - s.rv++ - job.ResourceVersion = strconv.FormatInt(s.rv, 10) - job.Status.State = provisioning.JobStatePending - job.CreationTimestamp = metav1.NewTime(time.Now()) - - jobs := make([]provisioning.Job, 0, len(s.jobs)+2) - jobs = append(jobs, *job) - for i, j := range s.jobs { - if i >= s.capacity { - // Remove the old jobs - s.watchSet.notifyWatchers(watch.Event{ - Object: j.DeepCopyObject(), - Type: watch.Deleted, - }, nil) - continue - } - jobs = append(jobs, j) - } - - // Send add event - s.watchSet.notifyWatchers(watch.Event{ - Object: job.DeepCopyObject(), - Type: watch.Added, - }, nil) - - // For now, start a thread processing each job - go s.drainPending() - - s.jobs = jobs // replace existing list - return job, nil -} - -// Reads the queue until no jobs remain -func (s *jobStore) drainPending() { - logger := logging.DefaultLogger.With("logger", "job-store") - ctx := logging.Context(context.Background(), logger) - - var err error - for { - time.Sleep(time.Microsecond * 200) - - job := s.Next(ctx) - if job == nil { - return // done - } - logger := logger.With("job", job.GetName(), "namespace", job.GetNamespace()) - ctx := logging.Context(ctx, logger) - - var foundWorker bool - recorder := newJobProgressRecorder(func(ctx context.Context, j provisioning.JobStatus) error { - return s.Update(ctx, job.Namespace, job.Name, j) - }) - - for _, worker := range s.workers { - if !worker.IsSupported(ctx, *job) { - continue - } - - // Already found a worker, no need to continue - foundWorker = true - err = s.processByWorker(ctx, worker, *job, recorder) - break - } - - if !foundWorker { - err = errors.New("no registered worker supports this job") - } - - status := recorder.Complete(ctx, err) - err = s.Update(ctx, job.Namespace, job.Name, status) - if err != nil { - logger.Error("error running job", "error", err) - } - logger.Debug("job has been fully completed") - } -} - -func (s *jobStore) processByWorker(ctx context.Context, worker Worker, job provisioning.Job, recorder JobProgressRecorder) error { - ctx = request.WithNamespace(ctx, job.Namespace) - ctx, _, err := identity.WithProvisioningIdentitiy(ctx, job.Namespace) - if err != nil { - return fmt.Errorf("get worker identity: %w", err) - } - repoName := job.Spec.Repository - - logger := logging.FromContext(ctx) - logger = logger.With("repository", repoName) - ctx = logging.Context(ctx, logger) - - repo, err := s.getter.GetRepository(ctx, repoName) - if err != nil { - return fmt.Errorf("get repository: %w", err) - } - - // TODO: does this really happen? - if repo == nil { - return errors.New("unknown repository") - } - - return worker.Process(ctx, repo, job, recorder) -} - -// Checkout the next "pending" job -func (s *jobStore) Next(ctx context.Context) *provisioning.Job { - s.mutex.Lock() - defer s.mutex.Unlock() - - // The oldest jobs should be checked out first - for i := len(s.jobs) - 1; i >= 0; i-- { - if s.jobs[i].Status.State == provisioning.JobStatePending { - oldObj := s.jobs[i].DeepCopyObject() - - s.rv++ - s.jobs[i].ResourceVersion = strconv.FormatInt(s.rv, 10) - s.jobs[i].Status.State = provisioning.JobStateWorking - s.jobs[i].Status.Started = time.Now().UnixMilli() - job := s.jobs[i] - - s.watchSet.notifyWatchers(watch.Event{ - Object: job.DeepCopyObject(), - Type: watch.Modified, - }, oldObj) - return &job - } - } - return nil -} - -func (s *jobStore) Update(ctx context.Context, namespace string, name string, status provisioning.JobStatus) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.rv++ - - if status.State == "" { - return apierrors.NewBadRequest("The state must be set") - } - if status.Progress > 100 || status.Progress < 0 { - return apierrors.NewBadRequest("progress must be between 0 and 100") - } - - for idx, job := range s.jobs { - if job.Name == name && job.Namespace == namespace { - if job.Status.State.Finished() { - return &apierrors.StatusError{ErrStatus: metav1.Status{ - Code: http.StatusPreconditionFailed, - Message: "The job is already finished and can not be updated", - }} - } - if status.State.Finished() { - status.Finished = time.Now().UnixMilli() - } - - oldObj := job.DeepCopyObject() - job.ResourceVersion = strconv.FormatInt(s.rv, 10) - job.Status = status - s.jobs[idx] = job - - s.watchSet.notifyWatchers(watch.Event{ - Object: job.DeepCopyObject(), - Type: watch.Modified, - }, oldObj) - return nil - } - } - - return apierrors.NewNotFound(provisioning.JobResourceInfo.GroupResource(), name) -} diff --git a/pkg/registry/apis/provisioning/jobs/sync/changes.go b/pkg/registry/apis/provisioning/jobs/sync/changes.go new file mode 100644 index 00000000000..312c023854b --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/sync/changes.go @@ -0,0 +1,88 @@ +package sync + +import ( + "fmt" + "sort" + + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +type ResourceFileChange struct { + // Path to the file in a repository with a change + Path string + Action repository.FileAction + + // The current value in the database -- only required for delete + Existing *provisioning.ResourceListItem +} + +func Changes(source []repository.FileTreeEntry, target *provisioning.ResourceList) ([]ResourceFileChange, error) { + lookup := make(map[string]*provisioning.ResourceListItem, len(target.Items)) + for _, item := range target.Items { + if item.Path == "" { + if item.Group != folders.GROUP { + return nil, fmt.Errorf("empty path on a non folder") + } + continue + } + lookup[item.Path] = &item + } + + keep := safepath.NewTrie() + changes := make([]ResourceFileChange, 0, len(source)) + for _, file := range source { + if !file.Blob { + continue // skip folder references? + } + + check, ok := lookup[file.Path] + if ok { + if check.Hash != file.Hash { + changes = append(changes, ResourceFileChange{ + Action: repository.FileActionUpdated, + Path: check.Path, + Existing: check, + }) + } + + if err := keep.Add(file.Path); err != nil { + return nil, fmt.Errorf("failed to add path to keep trie: %w", err) + } + + delete(lookup, file.Path) + continue + } + + // TODO: does this work with empty folders? + if resources.IsPathSupported(file.Path) == nil { + changes = append(changes, ResourceFileChange{ + Action: repository.FileActionCreated, // or previously ignored/failed + Path: file.Path, + }) + } + } + + // Paths found in grafana, without a matching path in the repository + for _, v := range lookup { + if v.Resource == folders.RESOURCE && keep.Exists(v.Path) { + continue + } + + changes = append(changes, ResourceFileChange{ + Action: repository.FileActionDeleted, + Path: v.Path, + Existing: v, + }) + } + + // Deepest first (stable sort order) + sort.Slice(changes, func(i, j int) bool { + return safepath.Depth(changes[i].Path) > safepath.Depth(changes[j].Path) + }) + + return changes, nil +} diff --git a/pkg/registry/apis/provisioning/jobs/sync/changes_test.go b/pkg/registry/apis/provisioning/jobs/sync/changes_test.go new file mode 100644 index 00000000000..94c415f958d --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/sync/changes_test.go @@ -0,0 +1,145 @@ +package sync + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +func TestChanges(t *testing.T) { + t.Run("start the same", func(t *testing.T) { + source, target := getBase(t) + changes, err := Changes(source, target) + require.NoError(t, err) + require.Empty(t, changes) + }) + + t.Run("create a source file", func(t *testing.T) { + source, target := getBase(t) + source = append(source, repository.FileTreeEntry{ + Path: "muta.json", Hash: "xyz", Blob: true, + }) + + changes, err := Changes(source, target) + require.NoError(t, err) + require.Len(t, changes, 1) + require.Equal(t, ResourceFileChange{ + Action: repository.FileActionCreated, + Path: "muta.json", + }, changes[0]) + }) + + t.Run("delete a source file", func(t *testing.T) { + source, target := getBase(t) + source = []repository.FileTreeEntry{source[0]} + + changes, err := Changes(source, target) + require.NoError(t, err) + require.Len(t, changes, 1) + require.Equal(t, ResourceFileChange{ + Action: repository.FileActionDeleted, + Path: "adsl62h.yaml", + Existing: &provisioning.ResourceListItem{ + Path: "adsl62h.yaml", + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "adsl62h-hrw-f-fvlt2dghp-gufrc4lisksgmq-c", + Hash: "ce5d497c4deadde6831162ce8509e2b2b1776237", + }, + }, changes[0]) + }) + + t.Run("folder deletion order", func(t *testing.T) { + source := []repository.FileTreeEntry{ + {Path: "x/y/z/ignored.md"}, // ignored + {Path: "aaa/bbb.yaml", Hash: "xyz", Blob: true}, + } + target := &provisioning.ResourceList{ + Items: []provisioning.ResourceListItem{ + {Path: "a.json"}, + {Path: "x/y/file.json"}, + {Path: "aaa/", Resource: "folders"}, // Folder... no action required + {Path: "aaa/bbb.yaml", Hash: "xyz"}, + {Path: "zzz/longest/path/here.json"}, + {Path: "short/file.yml"}, + }, + } + changes, err := Changes(source, target) + require.NoError(t, err) + + order := make([]string, len(changes)) + for i := range changes { + order[i] = changes[i].Path + } + require.Equal(t, []string{ + "zzz/longest/path/here.json", // not sorted yet + "x/y/file.json", + "short/file.yml", + "a.json", + }, order) + }) + + t.Run("modify a file", func(t *testing.T) { + source, target := getBase(t) + source[1].Hash = "different" + + changes, err := Changes(source, target) + require.NoError(t, err) + require.Len(t, changes, 1) + require.Equal(t, ResourceFileChange{ + Action: repository.FileActionUpdated, + Path: "adsl62h.yaml", + Existing: &provisioning.ResourceListItem{ + Path: "adsl62h.yaml", + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "adsl62h-hrw-f-fvlt2dghp-gufrc4lisksgmq-c", + Hash: "ce5d497c4deadde6831162ce8509e2b2b1776237", + }, + }, changes[0]) + }) +} + +func getBase(t *testing.T) (source []repository.FileTreeEntry, target *provisioning.ResourceList) { + target = &provisioning.ResourceList{} + err := json.Unmarshal([]byte(`{ + "kind": "ResourceList", + "apiVersion": "provisioning.grafana.app/v0alpha1", + "metadata": {}, + "items": [ + { + "path": "", + "group": "folder.grafana.app", + "resource": "folders", + "name": "simplelocal-3794ab9", + "hash": "" + }, + { + "path": "ad4lwp2.yaml", + "group": "dashboard.grafana.app", + "resource": "dashboards", + "name": "ad4lwp2-xofjsuo-mr5blr1zwimlfi0ds0pyrrpd", + "hash": "ca83d64b9c4a23fed975aacdf47e7de8878b4ae0" + }, + { + "path": "adsl62h.yaml", + "group": "dashboard.grafana.app", + "resource": "dashboards", + "name": "adsl62h-hrw-f-fvlt2dghp-gufrc4lisksgmq-c", + "hash": "ce5d497c4deadde6831162ce8509e2b2b1776237" + } + ] + }`), target) + require.NoError(t, err) + + source = []repository.FileTreeEntry{ + {Path: "ad4lwp2.yaml", Hash: "ca83d64b9c4a23fed975aacdf47e7de8878b4ae0", Blob: true}, + {Path: "adsl62h.yaml", Hash: "ce5d497c4deadde6831162ce8509e2b2b1776237", Blob: true}, + } + + return // named values! +} diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go new file mode 100644 index 00000000000..aa5757aabba --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -0,0 +1,476 @@ +package sync + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "sort" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + client "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" +) + +// SyncWorker synchronizes the external repo with grafana database +// this function updates the status for both the job and the referenced repository +type SyncWorker struct { + // Used to update the repository status with sync info + client client.ProvisioningV0alpha1Interface + + // Lists the values saved in grafana database + lister resources.ResourceLister + + // Parses fields saved in remore repository + parsers *resources.ParserFactory + + // Check if the system is using unified storage + storageStatus dualwrite.Service +} + +func NewSyncWorker( + client client.ProvisioningV0alpha1Interface, + parsers *resources.ParserFactory, + lister resources.ResourceLister, + storageStatus dualwrite.Service, +) *SyncWorker { + return &SyncWorker{ + client: client, + parsers: parsers, + lister: lister, + storageStatus: storageStatus, + } +} + +func (r *SyncWorker) IsSupported(ctx context.Context, job provisioning.Job) bool { + return job.Spec.Action == provisioning.JobActionSync +} + +func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error { + cfg := repo.Config() + logger := logging.FromContext(ctx).With("job", job.GetName(), "namespace", job.GetNamespace()) + // Check if we are onboarding from legacy storage + if dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, r.storageStatus) { + return fmt.Errorf("sync not supported until storage has migrated") + } + + rw, ok := repo.(repository.Reader) + if !ok { + return fmt.Errorf("sync job submitted for repository that does not support read-write -- this is a bug") + } + + syncStatus := job.Status.ToSyncStatus(job.Name) + // Preserve last ref as we use replace operation + syncStatus.LastRef = repo.Config().Status.Sync.LastRef + + // Update sync status at start using JSON patch + patchOperations := []map[string]interface{}{ + { + "op": "replace", + "path": "/status/sync", + "value": syncStatus, + }, + } + + progress.SetMessage(ctx, "update sync status at start") + if err := r.patchStatus(ctx, cfg, patchOperations); err != nil { + return fmt.Errorf("update repo with job status at start: %w", err) + } + + progress.SetMessage(ctx, "execute sync job") + syncJob, err := r.createJob(ctx, rw, progress) + if err != nil { + return fmt.Errorf("failed to create sync job: %w", err) + } + + syncError := syncJob.run(ctx, *job.Spec.Pull) + jobStatus := progress.Complete(ctx, syncError) + syncStatus = jobStatus.ToSyncStatus(job.Name) + + // Create sync status and set hash if successful + if syncStatus.State == provisioning.JobStateSuccess { + syncStatus.LastRef = progress.GetRef() + } + + // Update final status using JSON patch + progress.SetMessage(ctx, "update status and stats") + patchOperations = []map[string]interface{}{ + { + "op": "replace", + "path": "/status/sync", + "value": syncStatus, + }, + } + + // Only add stats patch if stats are not nil + if stats, err := r.lister.Stats(ctx, cfg.Namespace, cfg.Name); err != nil { + logger.Error("unable to read stats", "error", err) + } else if stats != nil && len(stats.Managed) == 1 { + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/stats", + "value": stats.Managed[0].Stats, + }) + } + + // Only patch the specific fields we want to update, not the entire status + if err := r.patchStatus(ctx, cfg, patchOperations); err != nil { + return fmt.Errorf("update repo with job final status: %w", err) + } + + return syncError +} + +// start a job and run it +func (r *SyncWorker) createJob(ctx context.Context, repo repository.Reader, progress jobs.JobProgressRecorder) (*syncJob, error) { + cfg := repo.Config() + parser, err := r.parsers.GetParser(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to get parser for %s: %w", cfg.Name, err) + } + + folderClient, err := parser.Clients().Folder() + if err != nil { + return nil, fmt.Errorf("unable to get folder client: %w", err) + } + + dashboardClient, err := parser.Clients().Dashboard() + if err != nil { + return nil, fmt.Errorf("unable to get dashboard client: %w", err) + } + + job := &syncJob{ + repository: repo, + progress: progress, + parser: parser, + lister: r.lister, + folders: resources.NewFolderManager(repo, folderClient), + dashboards: dashboardClient, + resourcesLookup: map[resourceID]string{}, + } + + return job, nil +} + +func (r *SyncWorker) patchStatus(ctx context.Context, repo *provisioning.Repository, patchOperations []map[string]interface{}) error { + patch, err := json.Marshal(patchOperations) + if err != nil { + return fmt.Errorf("unable to marshal patch data: %w", err) + } + + _, err = r.client.Repositories(repo.Namespace). + Patch(ctx, repo.Name, types.JSONPatchType, patch, metav1.PatchOptions{}, "status") + if err != nil { + return fmt.Errorf("unable to update repo with job status: %w", err) + } + + return nil +} + +type resourceID struct { + Name string + Resource string + Group string +} + +// created once for each sync execution +type syncJob struct { + repository repository.Reader + progress jobs.JobProgressRecorder + parser *resources.Parser + lister resources.ResourceLister + folders *resources.FolderManager + dashboards dynamic.ResourceInterface + folderLookup *resources.FolderTree + resourcesLookup map[resourceID]string // the path with this k8s name +} + +func (r *syncJob) run(ctx context.Context, options provisioning.SyncJobOptions) error { + // Ensure the configured folder exists and is managed by the repository + cfg := r.repository.Config() + rootFolder := resources.RootFolder(cfg) + if rootFolder != "" { + if err := r.folders.EnsureFolderExists(ctx, resources.Folder{ + ID: rootFolder, // will not change if exists + Title: cfg.Spec.Title, + Path: "", // at the root of the repository + }, ""); err != nil { + return fmt.Errorf("unable to create root folder: %w", err) + } + } + + var err error + var currentRef string + + versionedRepo, _ := r.repository.(repository.Versioned) + if versionedRepo != nil { + currentRef, err = versionedRepo.LatestRef(ctx) + if err != nil { + return fmt.Errorf("getting latest ref: %w", err) + } + r.progress.SetRef(currentRef) + + if cfg.Status.Sync.LastRef != "" && options.Incremental { + if currentRef == cfg.Status.Sync.LastRef { + r.progress.SetFinalMessage(ctx, "same commit as last sync") + return nil + } + + return r.applyVersionedChanges(ctx, versionedRepo, cfg.Status.Sync.LastRef, currentRef) + } + } + + // Read the complete change set + target, err := r.lister.List(ctx, cfg.Namespace, cfg.Name) + if err != nil { + return fmt.Errorf("error listing current: %w", err) + } + source, err := r.repository.ReadTree(ctx, currentRef) + if err != nil { + return fmt.Errorf("error reading tree: %w", err) + } + changes, err := Changes(source, target) + if err != nil { + return fmt.Errorf("error calculating changes: %w", err) + } + + if len(changes) == 0 { + r.progress.SetFinalMessage(ctx, "no changes to sync") + return nil + } + + // Load any existing folder information + r.folderLookup = resources.NewFolderTreeFromResourceList(target) + + // Now apply the changes + return r.applyChanges(ctx, changes) +} + +func (r *syncJob) applyChanges(ctx context.Context, changes []ResourceFileChange) error { + if len(r.resourcesLookup) > 0 { + return fmt.Errorf("this should be empty") + } + + // Do the deepest paths first (important for delete) + sort.Slice(changes, func(i, j int) bool { + return safepath.Depth(changes[i].Path) > safepath.Depth(changes[j].Path) + }) + + r.progress.SetTotal(ctx, len(changes)) + r.progress.SetMessage(ctx, "replicating changes") + + // Create folder structure first + for _, change := range changes { + if err := r.progress.TooManyErrors(); err != nil { + return err + } + + if change.Action == repository.FileActionDeleted { + result := jobs.JobResourceResult{ + Name: change.Existing.Name, + Resource: change.Existing.Resource, + Group: change.Existing.Group, + Path: change.Path, + Action: change.Action, + } + + if change.Existing == nil || change.Existing.Name == "" { + result.Error = errors.New("missing existing reference") + r.progress.Record(ctx, result) + continue + } + + client, err := r.client(change.Existing.Resource) + if err != nil { + result.Error = fmt.Errorf("unable to get client for deleted object: %w", err) + r.progress.Record(ctx, result) + continue + } + + result.Error = client.Delete(ctx, change.Existing.Name, metav1.DeleteOptions{}) + r.progress.Record(ctx, result) + continue + } + + // Write the resource file + r.progress.Record(ctx, r.writeResourceFromFile(ctx, change.Path, "", change.Action)) + } + + r.progress.SetMessage(ctx, "changes replicated") + + return nil +} + +// Convert git changes into resource file changes +func (r *syncJob) applyVersionedChanges(ctx context.Context, repo repository.Versioned, previousRef, currentRef string) error { + diff, err := repo.CompareFiles(ctx, previousRef, currentRef) + if err != nil { + return fmt.Errorf("compare files error: %w", err) + } + + if len(diff) < 1 { + r.progress.SetFinalMessage(ctx, "no changes detected between commits") + return nil + } + + r.progress.SetTotal(ctx, len(diff)) + r.progress.SetMessage(ctx, "replicating versioned changes") + + for _, change := range diff { + if err := r.progress.TooManyErrors(); err != nil { + return err + } + + if err := resources.IsPathSupported(change.Path); err != nil { + r.progress.Record(ctx, jobs.JobResourceResult{ + Path: change.Path, + Action: repository.FileActionIgnored, + }) + continue + } + + switch change.Action { + case repository.FileActionCreated, repository.FileActionUpdated: + r.progress.Record(ctx, r.writeResourceFromFile(ctx, change.Path, change.Ref, change.Action)) + case repository.FileActionDeleted: + r.progress.Record(ctx, r.deleteObject(ctx, change.Path, change.PreviousRef)) + case repository.FileActionRenamed: + // 1. Delete + result := r.deleteObject(ctx, change.Path, change.PreviousRef) + if result.Error != nil { + r.progress.Record(ctx, result) + continue + } + + // 2. Create + r.progress.Record(ctx, r.writeResourceFromFile(ctx, change.Path, change.Ref, repository.FileActionCreated)) + case repository.FileActionIgnored: + r.progress.Record(ctx, jobs.JobResourceResult{ + Path: change.Path, + Action: repository.FileActionIgnored, + }) + } + } + + r.progress.SetMessage(ctx, "versioned changes replicated") + + return nil +} + +func (r *syncJob) deleteObject(ctx context.Context, path string, ref string) jobs.JobResourceResult { + info, err := r.repository.Read(ctx, path, ref) + result := jobs.JobResourceResult{ + Path: path, + Action: repository.FileActionDeleted, + } + + if err != nil { + result.Error = fmt.Errorf("failed to read file: %w", err) + return result + } + + obj, gvk, _ := resources.DecodeYAMLObject(bytes.NewBuffer(info.Data)) + if obj == nil { + result.Error = errors.New("no object found") + return result + } + + objName := obj.GetName() + if objName == "" { + // Find the referenced file + objName, _ = resources.NamesFromHashedRepoPath(r.repository.Config().Name, path) + } + + result.Name = objName + result.Resource = gvk.Kind + result.Group = gvk.Group + + client, err := r.client(gvk.Kind) + if err != nil { + result.Error = fmt.Errorf("unable to get client for deleted object: %w", err) + return result + } + + err = client.Delete(ctx, objName, metav1.DeleteOptions{}) + if err != nil { + result.Error = fmt.Errorf("failed to delete: %w", err) + return result + } + + return result +} + +func (r *syncJob) writeResourceFromFile(ctx context.Context, path string, ref string, action repository.FileAction) jobs.JobResourceResult { + result := jobs.JobResourceResult{ + Path: path, + Action: action, + } + + // Read the referenced file + fileInfo, err := r.repository.Read(ctx, path, ref) + if err != nil { + result.Error = fmt.Errorf("failed to read file: %w", err) + return result + } + + parsed, err := r.parser.Parse(ctx, fileInfo, false) // no validation + if err != nil { + result.Error = fmt.Errorf("failed to parse file: %w", err) + return result + } + + // Check if the resource already exists + id := resourceID{ + Name: parsed.Obj.GetName(), + Resource: parsed.GVR.Resource, + Group: parsed.GVK.Group, + } + existing, found := r.resourcesLookup[id] + if found { + result.Error = fmt.Errorf("duplicate resource name: %s, %s and %s", parsed.Obj.GetName(), path, existing) + return result + } + r.resourcesLookup[id] = path + + // Make sure the parent folders exist + folder, err := r.folders.EnsureFolderPathExist(ctx, path) + if err != nil { + result.Error = fmt.Errorf("failed to ensure folder path exists: %w", err) + return result + } + + parsed.Meta.SetFolder(folder) + parsed.Meta.SetUID("") // clear identifiers + parsed.Meta.SetResourceVersion("") // clear identifiers + + result.Name = parsed.Obj.GetName() + result.Resource = parsed.GVR.Resource + result.Group = parsed.GVK.Group + + // Update will also create (for resources we care about) + _, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{}) + result.Error = err + return result +} + +func (r *syncJob) client(kind string) (dynamic.ResourceInterface, error) { + switch kind { + case dashboard.GROUP, dashboard.DASHBOARD_RESOURCE, "Dashboard": + return r.dashboards, nil + case folders.GROUP, folders.RESOURCE, "Folder": + return r.folders.Client(), nil + } + return nil, fmt.Errorf("unsupported resource: %s", kind) +} diff --git a/pkg/registry/apis/provisioning/jobs/watchset.go b/pkg/registry/apis/provisioning/jobs/watchset.go deleted file mode 100644 index f9a2934482d..00000000000 --- a/pkg/registry/apis/provisioning/jobs/watchset.go +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Provenance-includes-location: https://github.com/tilt-dev/tilt-apiserver/blob/main/pkg/storage/filepath/watchset.go -// Provenance-includes-license: Apache-2.0 -// Provenance-includes-copyright: The Kubernetes Authors. - -// See also -// https://github.com/grafana/grafana/blob/v11.1.9/pkg/apiserver/storage/file/watchset.go - -package jobs - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/storage" - "k8s.io/klog/v2" -) - -const ( - UpdateChannelSize = 25 - InitialWatchNodesSize = 20 - InitialBufferedEventsSize = 25 -) - -type eventWrapper struct { - ev watch.Event - // optional: oldObject is only set for modifications for determining their type as necessary (when using predicate filtering) - oldObject runtime.Object -} - -type watchNode struct { - ctx context.Context - s *WatchSet - id uint64 - updateCh chan eventWrapper - outCh chan watch.Event - requestedRV uint64 - // the watch may or may not be namespaced for a namespaced resource. This is always nil for cluster-scoped kinds - watchNamespace *string - predicate storage.SelectionPredicate - versioner storage.Versioner -} - -// Keeps track of which watches need to be notified -type WatchSet struct { - mu sync.RWMutex - // mu protects both nodes and counter - nodes map[uint64]*watchNode - counter atomic.Uint64 - buffered []eventWrapper - bufferedMutex sync.RWMutex -} - -func NewWatchSet() *WatchSet { - return &WatchSet{ - buffered: make([]eventWrapper, 0, InitialBufferedEventsSize), - nodes: make(map[uint64]*watchNode, InitialWatchNodesSize), - } -} - -// Creates a new watch with a unique id, but -// does not start sending events to it until start() is called. -func (s *WatchSet) newWatch(ctx context.Context, requestedRV uint64, p storage.SelectionPredicate, versioner storage.Versioner, namespace *string) *watchNode { - s.counter.Add(1) - - node := &watchNode{ - ctx: ctx, - requestedRV: requestedRV, - id: s.counter.Load(), - s: s, - // updateCh size needs to be > 1 to allow slower clients to not block passing new events - updateCh: make(chan eventWrapper, UpdateChannelSize), - // outCh size needs to be > 1 for single process use-cases such as tests where watch and event seeding from CUD - // events is happening on the same thread - outCh: make(chan watch.Event, UpdateChannelSize), - predicate: p, - watchNamespace: namespace, - versioner: versioner, - } - - return node -} - -func (s *WatchSet) CleanupWatchers() { - s.mu.Lock() - defer s.mu.Unlock() - for _, w := range s.nodes { - w.stop() - } -} - -// oldObject is only passed in the event of a modification -// in case a predicate filtered watch is impacted as a result of modification -// NOTE: this function gives one the misperception that a newly added node will never -// get a double event, one from buffered and one from the update channel -// That perception is not true. Even though this function maintains the lock throughout the function body -// it is not true of the Start function. So basically, the Start function running after this function -// fully stands the chance of another future notifyWatchers double sending it the event through the two means mentioned -func (s *WatchSet) notifyWatchers(ev watch.Event, oldObject runtime.Object) { - s.mu.RLock() - defer s.mu.RUnlock() - - updateEv := eventWrapper{ - ev: ev, - } - if oldObject != nil { - updateEv.oldObject = oldObject - } - - // Events are always buffered. - // this is because of an inadvertent delay which is built into the watch process - // Watch() from storage returns Watch.Interface with a async start func. - // The only way to guarantee that we can interpret the passed RV correctly is to play it against missed events - // (notice the loop below over s.nodes isn't exactly going to work on a new node - // unless start is called on it) - s.bufferedMutex.Lock() - s.buffered = append(s.buffered, updateEv) - s.bufferedMutex.Unlock() - - for _, w := range s.nodes { - w.updateCh <- updateEv - } -} - -// isValid is not necessary to be called on oldObject in UpdateEvents - assuming the Watch pushes correctly setup eventWrapper our way -// first bool is whether the event is valid for current watcher -// second bool is whether checking the old value against the predicate may be valuable to the caller -// second bool may be a helpful aid to establish context around MODIFIED events -// (note that this second bool is only marked true if we pass other checks first, namely RV and namespace) -func (w *watchNode) isValid(e eventWrapper) (bool, bool, error) { - obj, err := meta.Accessor(e.ev.Object) - if err != nil { - klog.Error("Could not get accessor to object in event") - return false, false, nil - } - - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - return false, false, err - } - - if eventRV < w.requestedRV { - return false, false, nil - } - - if w.watchNamespace != nil && *w.watchNamespace != obj.GetNamespace() { - return false, false, err - } - - valid, err := w.predicate.Matches(e.ev.Object) - if err != nil { - return false, false, err - } - - return valid, e.ev.Type == watch.Modified, nil -} - -// Only call this method if current object matches the predicate -func (w *watchNode) handleAddedForFilteredList(e eventWrapper) (*watch.Event, error) { - if e.oldObject == nil { - return nil, fmt.Errorf("oldObject should be set for modified events") - } - - ok, err := w.predicate.Matches(e.oldObject) - if err != nil { - return nil, err - } - - if !ok { - e.ev.Type = watch.Added - return &e.ev, nil - } - - return nil, nil -} - -func (w *watchNode) handleDeletedForFilteredList(e eventWrapper) (*watch.Event, error) { - if e.oldObject == nil { - return nil, fmt.Errorf("oldObject should be set for modified events") - } - - ok, err := w.predicate.Matches(e.oldObject) - if err != nil { - return nil, err - } - - if !ok { - return nil, nil - } - - // isn't a match but used to be - e.ev.Type = watch.Deleted - - oldObjectAccessor, err := meta.Accessor(e.oldObject) - if err != nil { - klog.Errorf("Could not get accessor to correct the old RV of filtered out object") - return nil, err - } - - currentRV, err := getResourceVersion(e.ev.Object) - if err != nil { - klog.Errorf("Could not get accessor to object in event") - return nil, err - } - - oldObjectAccessor.SetResourceVersion(currentRV) - e.ev.Object = e.oldObject - - return &e.ev, nil -} - -func (w *watchNode) processEvent(e eventWrapper, isInitEvent bool) error { - if isInitEvent { - // Init events have already been vetted against the predicate and other RV behavior - // Let them pass through - w.outCh <- e.ev - return nil - } - - valid, runDeleteFromFilteredListHandler, err := w.isValid(e) - if err != nil { - klog.Errorf("Could not determine validity of the event: %v", err) - return err - } - if valid { - if e.ev.Type == watch.Modified { - ev, err := w.handleAddedForFilteredList(e) - if err != nil { - return err - } - if ev != nil { - w.outCh <- *ev - } else { - // forward the original event if add handling didn't signal any impact - w.outCh <- e.ev - } - } else { - w.outCh <- e.ev - } - return nil - } - - if runDeleteFromFilteredListHandler { - if e.ev.Type == watch.Modified { - ev, err := w.handleDeletedForFilteredList(e) - if err != nil { - return err - } - if ev != nil { - w.outCh <- *ev - } - } // explicitly doesn't have an event forward for the else case here - return nil - } - - return nil -} - -// Start sending events to this watch. -func (w *watchNode) Start(initEvents ...watch.Event) { - w.s.mu.Lock() - w.s.nodes[w.id] = w - w.s.mu.Unlock() - - go func() { - maxRV := uint64(0) - for _, ev := range initEvents { - currentRV, err := w.getResourceVersionAsInt(ev.Object) - if err != nil { - klog.Errorf("Could not determine init event RV for deduplication of buffered events: %v", err) - continue - } - - if maxRV < currentRV { - maxRV = currentRV - } - - if err := w.processEvent(eventWrapper{ev: ev}, true); err != nil { - klog.Errorf("Could not process event: %v", err) - } - } - - // If we had no init events, simply rely on the passed RV - if maxRV == 0 { - maxRV = w.requestedRV - } - - w.s.bufferedMutex.RLock() - for _, e := range w.s.buffered { - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - klog.Errorf("Could not determine RV for deduplication of buffered events: %v", err) - continue - } - - if maxRV >= eventRV { - continue - } else { - maxRV = eventRV - } - - if err := w.processEvent(e, false); err != nil { - klog.Errorf("Could not process event: %v", err) - } - } - w.s.bufferedMutex.RUnlock() - - for { - select { - case e, ok := <-w.updateCh: - if !ok { - close(w.outCh) - return - } - - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - klog.Errorf("Could not determine RV for deduplication of channel events: %v", err) - continue - } - - if maxRV >= eventRV { - continue - } else { - maxRV = eventRV - } - - if err := w.processEvent(e, false); err != nil { - klog.Errorf("Could not process event: %v", err) - } - case <-w.ctx.Done(): - close(w.outCh) - return - } - } - }() -} - -func (w *watchNode) Stop() { - w.s.mu.Lock() - defer w.s.mu.Unlock() - w.stop() -} - -// Unprotected func: ensure mutex on the parent watch set is locked before calling -func (w *watchNode) stop() { - if _, ok := w.s.nodes[w.id]; ok { - delete(w.s.nodes, w.id) - close(w.updateCh) - } -} - -func (w *watchNode) ResultChan() <-chan watch.Event { - return w.outCh -} - -func getResourceVersion(obj runtime.Object) (string, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - klog.Error("Could not get accessor to object in event") - return "", err - } - return accessor.GetResourceVersion(), nil -} - -func (w *watchNode) getResourceVersionAsInt(obj runtime.Object) (uint64, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - klog.Error("Could not get accessor to object in event") - return 0, err - } - - return w.versioner.ParseResourceVersion(accessor.GetResourceVersion()) -} diff --git a/pkg/registry/apis/provisioning/list.go b/pkg/registry/apis/provisioning/list.go new file mode 100644 index 00000000000..6fc0a13c72d --- /dev/null +++ b/pkg/registry/apis/provisioning/list.go @@ -0,0 +1,66 @@ +package provisioning + +import ( + "context" + "fmt" + "net/http" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +// TODO: Rename to resources as it's not clear that we are returning here is repository resources +type listConnector struct { + getter RepoGetter + lister resources.ResourceLister +} + +func (*listConnector) New() runtime.Object { + return &provisioning.ResourceList{} +} + +func (*listConnector) Destroy() {} + +func (*listConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (*listConnector) ProducesObject(verb string) any { + return &provisioning.ResourceList{} +} + +func (*listConnector) ConnectMethods() []string { + return []string{http.MethodGet} +} + +func (*listConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (s *listConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + ns, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing namespace") + } + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO: Add pagination to resource lister + rsp, err := s.lister.List(ctx, ns, name) + if err != nil { + responder.Error(err) + } else { + responder.Object(200, rsp) + } + }), 30*time.Second), nil +} + +var ( + _ rest.Storage = (*listConnector)(nil) + _ rest.Connecter = (*listConnector)(nil) + _ rest.StorageMetadata = (*listConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/migrate.go b/pkg/registry/apis/provisioning/migrate.go new file mode 100644 index 00000000000..03bad2ee690 --- /dev/null +++ b/pkg/registry/apis/provisioning/migrate.go @@ -0,0 +1,89 @@ +package provisioning + +import ( + "context" + "net/http" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" +) + +// TODO: should we have merge migrate and sync connectors and have a single repository job connector? +type migrateConnector struct { + dual dualwrite.Service + repoGetter RepoGetter + jobs jobs.Queue +} + +func (*migrateConnector) New() runtime.Object { + return &provisioning.Job{} +} + +func (*migrateConnector) Destroy() {} + +func (*migrateConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (c *migrateConnector) ProducesObject(verb string) any { + return c.New() +} + +func (*migrateConnector) ConnectMethods() []string { + return []string{http.MethodPost} +} + +func (*migrateConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (c *migrateConnector) Connect( + ctx context.Context, + name string, + opts runtime.Object, + responder rest.Responder, +) (http.Handler, error) { + repo, err := c.repoGetter.GetHealthyRepository(ctx, name) + if err != nil { + return nil, err + } + + cfg := repo.Config() + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var options provisioning.MigrateJobOptions + if err := unmarshalJSON(r, defaultMaxBodySize, &options); err != nil { + responder.Error(apierrors.NewBadRequest("error decoding MigrateJobOptions from request")) + return + } + + job, err := c.jobs.Insert(ctx, &provisioning.Job{ + ObjectMeta: v1.ObjectMeta{ + Namespace: cfg.Namespace, + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMigrate, + Repository: cfg.Name, + Migrate: &options, + }, + }) + if err != nil { + responder.Error(err) + return + } + responder.Object(http.StatusAccepted, job) + }), 30*time.Second), nil +} + +var ( + _ rest.Connecter = (*migrateConnector)(nil) + _ rest.Storage = (*migrateConnector)(nil) + _ rest.StorageMetadata = (*migrateConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index d5a12275733..1abb955522c 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -3,17 +3,16 @@ package provisioning import ( "context" "fmt" + "net/http" + "path/filepath" + "strings" + "time" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/featuremgmt" - grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" + "github.com/prometheus/client_golang/prometheus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" @@ -23,38 +22,134 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + apiutils "github.com/grafana/grafana/pkg/apimachinery/utils" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/readonly" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + clientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" + informers "github.com/grafana/grafana/pkg/generated/informers/externalversions" + listers "github.com/grafana/grafana/pkg/generated/listers/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/pullrequest" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/rendering" + grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) +const repoControllerWorkers = 1 + var ( _ builder.APIGroupBuilder = (*APIBuilder)(nil) _ builder.APIGroupMutation = (*APIBuilder)(nil) _ builder.APIGroupValidation = (*APIBuilder)(nil) + _ builder.APIGroupRouteProvider = (*APIBuilder)(nil) _ builder.APIGroupPostStartHookProvider = (*APIBuilder)(nil) _ builder.OpenAPIPostProcessor = (*APIBuilder)(nil) ) type APIBuilder struct { - secrets secrets.Service - jobs jobs.JobQueue - getter rest.Getter + urlProvider func(namespace string) string + webhookSecretKey string + isPublic bool + + features featuremgmt.FeatureToggles + getter rest.Getter + localFileResolver *repository.LocalFolderResolver + render rendering.Service + parsers *resources.ParserFactory + ghFactory *github.Factory + clonedir string // where repo clones are managed + jobs interface { + jobs.Queue + jobs.Store + } + tester *RepositoryTester + resourceLister resources.ResourceLister + repositoryLister listers.RepositoryLister + legacyMigrator legacy.LegacyMigrator + storageStatus dualwrite.Service + unified resource.ResourceClient + secrets secrets.Service } // NewAPIBuilder creates an API builder. // It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line. // This means there are no hidden dependencies, and no use of e.g. *settings.Cfg. func NewAPIBuilder( + local *repository.LocalFolderResolver, + urlProvider func(namespace string) string, + webhookSecretKey string, + features featuremgmt.FeatureToggles, + render rendering.Service, + unified resource.ResourceClient, + clonedir string, // where repo clones are managed + configProvider apiserver.RestConfigProvider, + ghFactory *github.Factory, + legacyMigrator legacy.LegacyMigrator, + storageStatus dualwrite.Service, secrets secrets.Service, ) *APIBuilder { + // HACK: Assume is only public if it is HTTPS + isPublic := strings.HasPrefix(urlProvider(""), "https://") + return &APIBuilder{ - secrets: secrets, + urlProvider: urlProvider, + localFileResolver: local, + webhookSecretKey: webhookSecretKey, + isPublic: isPublic, + features: features, + ghFactory: ghFactory, + parsers: &resources.ParserFactory{ + ClientFactory: resources.NewClientFactory(configProvider), + }, + render: render, + clonedir: clonedir, + resourceLister: resources.NewResourceLister(unified, unified, legacyMigrator, storageStatus), + legacyMigrator: legacyMigrator, + storageStatus: storageStatus, + unified: unified, + secrets: secrets, } } // RegisterAPIService returns an API builder, from [NewAPIBuilder]. It is called by Wire. // This function happily uses services core to Grafana, and does not need to be multi-tenancy-compatible. func RegisterAPIService( + // It is OK to use setting.Cfg here -- this is only used when running single tenant with a full setup + cfg *setting.Cfg, features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, + reg prometheus.Registerer, + render rendering.Service, + client resource.ResourceClient, // implements resource.RepositoryClient + configProvider apiserver.RestConfigProvider, + ghFactory *github.Factory, + legacyMigrator legacy.LegacyMigrator, + storageStatus dualwrite.Service, + usageStatsService usagestats.Service, + // FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service! secretsSvc grafanasecrets.Service, ) (*APIBuilder, error) { if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) && @@ -62,18 +157,142 @@ func RegisterAPIService( return nil, nil // skip registration unless opting into experimental apis OR the feature specifically } - builder := NewAPIBuilder(secrets.NewSingleTenant(secretsSvc)) + folderResolver := &repository.LocalFolderResolver{ + PermittedPrefixes: cfg.PermittedProvisioningPaths, + HomePath: safepath.Clean(cfg.HomePath), + } + urlProvider := func(namespace string) string { + return cfg.AppURL + } + + builder := NewAPIBuilder(folderResolver, urlProvider, cfg.SecretKey, features, + render, client, + filepath.Join(cfg.DataPath, "clone"), // where repositories are cloned (temporarialy for now) + configProvider, ghFactory, + legacyMigrator, storageStatus, + secrets.NewSingleTenant(secretsSvc), + ) apiregistration.RegisterAPI(builder) + usageStatsService.RegisterMetricsFunc(builder.collectProvisioningStats) return builder, nil } +// TODO: Move specific endpoint authorization together with the rest of the logic. +// so that things are not spread out all over the place. func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.AuthorizerFunc( - func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - // TODO: Implement a webhook authoriser somehow. + func(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { + if identity.IsServiceIdentity(ctx) { + // A Grafana sub-system should have full access. We trust them to make wise decisions. + return authorizer.DecisionAllow, "", nil + } - // fallback to the standard authorizer - return authorizer.DecisionNoOpinion, "", nil + // Different routes may need different permissions. + // * Reading and modifying a repository's configuration requires administrator privileges. + // * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges. + // * Reading a repository's files requires viewer privileges. + // * Editing a repository's files requires editor privileges. + // * Syncing a repository requires editor privileges. + // * Exporting a repository requires administrator privileges. + // * Migrating a repository requires administrator privileges. + // * Testing a repository configuration requires administrator privileges. + // * Viewing a repository's history requires editor privileges. + + id, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "failed to find requester", err + } + + switch a.GetResource() { + case provisioning.RepositoryResourceInfo.GetName(): + // TODO: Support more fine-grained permissions than the basic roles. Especially on Enterprise. + switch a.GetSubresource() { + case "": + // Doing something with the repository itself. + if id.GetOrgRole().Includes(identity.RoleAdmin) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "admin role is required", nil + + case "test", "export", "migrate": + // Testing doesn't make sense for non-admins. + // Exporting and migrating are potentially dangerous. + if id.GetOrgRole().Includes(identity.RoleAdmin) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "admin role is required", nil + + case "webhook": + // When the resource is a webhook, we'll deal with permissions manually by checking signatures or similar in the webhook handler. + // The user in this context is usually an anonymous user, but may also be an authenticated synthetic check by the Grafana instance's operator as well. + // For context on the anonymous user, check the authn/clients/provisioning.go file. + return authorizer.DecisionAllow, "", nil + + case "files": + // Reading files is allowed for everyone, so as to allow code reviews and similar. + // Writing files is only allowed fo Editor and higher. + isViewer := id.GetOrgRole().Includes(identity.RoleViewer) + isEditor := id.GetOrgRole().Includes(identity.RoleEditor) + isReadOperation := a.GetVerb() == apiutils.VerbGet + + if isEditor || (isViewer && isReadOperation) { + return authorizer.DecisionAllow, "", nil + } else if isReadOperation { + return authorizer.DecisionDeny, "viewer role is required for reads", nil + } else { + return authorizer.DecisionDeny, "editor role is required for edits", nil + } + + case "render": + // This is used to read a blob from unified storage, for GitHub PR comments. + // GH uses a proxy for all images, so we need to accept it, always. + return authorizer.DecisionAllow, "", nil + + case "resources", "sync", "history": + // These are strictly read operations. + // Sync can also be somewhat destructive, but it's expected to be fine to import changes. + if id.GetOrgRole().Includes(identity.RoleEditor) { + return authorizer.DecisionAllow, "", nil + } else { + return authorizer.DecisionDeny, "editor role is required", nil + } + + default: + if id.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil + } + + case "stats": + // This can leak information one shouldn't necessarily have access to. + if id.GetOrgRole().Includes(identity.RoleAdmin) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "admin role is required", nil + + case "settings": + // This is strictly a read operation. It is handy on the frontend for viewers. + if id.GetOrgRole().Includes(identity.RoleViewer) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "viewer role is required", nil + + case provisioning.JobResourceInfo.GetName(), + provisioning.HistoricJobResourceInfo.GetName(): + // Jobs are shown on the configuration page. + if id.GetOrgRole().Includes(identity.RoleAdmin) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "admin role is required", nil + + default: + // We haven't bothered with this kind yet. + if id.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "unmapped kind defaults to no access", nil + } }) } @@ -103,21 +322,74 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI if err != nil { return fmt.Errorf("failed to create repository storage: %w", err) } - - // FIXME: Make job queue store the jobs somewhere persistent. - jobStore := jobs.NewJobStore(50, b) // in memory, for now... - b.jobs = jobStore - repositoryStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, repositoryStorage) + b.getter = repositoryStorage + + realJobStore, err := grafanaregistry.NewCompleteRegistryStore(opts.Scheme, provisioning.JobResourceInfo, opts.OptsGetter) + if err != nil { + return fmt.Errorf("failed to create job storage: %w", err) + } + + historicJobStore, err := grafanaregistry.NewCompleteRegistryStore(opts.Scheme, provisioning.HistoricJobResourceInfo, opts.OptsGetter) + if err != nil { + return fmt.Errorf("failed to create historic job storage: %w", err) + } + + b.jobs, err = jobs.NewStore(realJobStore, historicJobStore, time.Second*30) + if err != nil { + return fmt.Errorf("failed to create job store: %w", err) + } storage := map[string]rest.Storage{} - storage[provisioning.JobResourceInfo.StoragePath()] = jobStore + // Although we never interact with these resources via the API, we want them to be readable from the API. + storage[provisioning.JobResourceInfo.StoragePath()] = readonly.Wrap(realJobStore) + storage[provisioning.HistoricJobResourceInfo.StoragePath()] = readonly.Wrap(historicJobStore) + storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage + + // TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place + // TODO: Do not set private fields directly, use factory methods. + storage[provisioning.RepositoryResourceInfo.StoragePath("webhook")] = &webhookConnector{ + getter: b, + jobs: b.jobs, + webhooksEnabled: b.isPublic, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = &testConnector{ + getter: b, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = &filesConnector{ + getter: b, + parsers: b.parsers, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{ + getter: b, + lister: b.resourceLister, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("history")] = &historySubresource{ + repoGetter: b, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("sync")] = &syncConnector{ + repoGetter: b, + jobs: b.jobs, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("export")] = &exportConnector{ + repoGetter: b, + jobs: b.jobs, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("migrate")] = &migrateConnector{ + repoGetter: b, + jobs: b.jobs, + dual: b.storageStatus, + } + storage[provisioning.RepositoryResourceInfo.StoragePath("render")] = &renderConnector{ + blob: b.unified, + } apiGroupInfo.VersionedResourcesStorageMap[provisioning.VERSION] = storage return nil } +// TODO: Move this to a more appropriate place. Probably controller/mutation.go func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { obj := a.GetObject() @@ -130,20 +402,102 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis return fmt.Errorf("expected repository configuration") } - // TODO: Do something based on the resource we got. - _ = r + // This is called on every update, so be careful to only add the finalizer for create + if len(r.Finalizers) == 0 && a.GetOperation() == admission.Create { + r.Finalizers = []string{ + controller.RemoveOrphanResourcesFinalizer, + controller.CleanFinalizer, + } + } + + if r.Spec.Sync.IntervalSeconds == 0 { + r.Spec.Sync.IntervalSeconds = 60 + } + + // TODO: move this logic into github repository concrete implementation. + if r.Spec.Type == provisioning.GitHubRepositoryType { + if r.Spec.GitHub == nil { + return fmt.Errorf("github configuration is required") + } + + // Trim trailing slash or .git + if len(r.Spec.GitHub.URL) > 5 { + r.Spec.GitHub.URL = strings.TrimRight(strings.TrimRight(r.Spec.GitHub.URL, "/"), ".git") + } + } + + if r.Spec.Workflows == nil { + r.Spec.Workflows = []provisioning.Workflow{} + } + + if err := b.encryptSecrets(ctx, r); err != nil { + return fmt.Errorf("failed to encrypt secrets: %w", err) + } return nil } +// TODO: move logic to a more appropriate place. Probably controller/validation.go func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { obj := a.GetObject() - if obj == nil || a.GetOperation() == admission.Connect { + if obj == nil || a.GetOperation() == admission.Connect || a.GetOperation() == admission.Delete { return nil // This is normal for sub-resource } - var list field.ErrorList - // TODO: Fill the list with validation errors. + // Do not validate objects we are trying to delete + meta, _ := apiutils.MetaAccessor(obj) + if meta.GetDeletionTimestamp() != nil { + return nil + } + + repo, err := b.asRepository(ctx, obj) + if err != nil { + return err + } + + list := repository.ValidateRepository(repo) + cfg := repo.Config() + + if a.GetOperation() == admission.Update { + oldRepo, err := b.asRepository(ctx, a.GetOldObject()) + if err != nil { + return fmt.Errorf("get old repository for update: %w", err) + } + oldCfg := oldRepo.Config() + + if cfg.Spec.Type != oldCfg.Spec.Type { + list = append(list, field.Forbidden(field.NewPath("spec", "type"), + "Changing repository type is not supported")) + } + + // Do not allow changing the sync target once anything has synced successfully + if cfg.Spec.Sync.Target != oldCfg.Spec.Sync.Target && len(cfg.Status.Stats) > 0 { + list = append(list, field.Forbidden(field.NewPath("spec", "sync", "target"), + "Changing sync target after running sync is not supported")) + } + } + + targetError := b.verifyAgaintsExistingRepositories(cfg) + if targetError != nil { + list = append(list, targetError) + } + + // For *create* we do a synchronous test... this can be expensive! + // it is the same as a full healthcheck, so should not be run on every update + if len(list) == 0 && a.GetOperation() == admission.Create { + testResults, err := repository.TestRepository(ctx, repo) + if err != nil { + list = append(list, field.Invalid(field.NewPath("spec"), + "Repository test failed", "Unable to verify repository: "+err.Error())) + } + + if !testResults.Success { + for _, err := range testResults.Errors { + list = append(list, field.Invalid(field.NewPath("spec"), + "Repository test failed", err)) + } + } + } if len(list) > 0 { return apierrors.NewInvalid( @@ -153,43 +507,619 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm return nil } -func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return provisioning.GetOpenAPIDefinitions +// TODO: move this to a more appropriate place. Probably controller/validation.go +func (b *APIBuilder) verifyAgaintsExistingRepositories(cfg *provisioning.Repository) *field.Error { + all, err := b.repositoryLister.Repositories(cfg.Namespace).List(labels.Everything()) + if err != nil { + return field.Forbidden(field.NewPath("spec"), + "Unable to verify root target: "+err.Error()) + } + + if cfg.Spec.Sync.Target == provisioning.SyncTargetTypeInstance { + for _, v := range all { + if v.Name != cfg.Name && v.Spec.Sync.Target == provisioning.SyncTargetTypeInstance { + return field.Forbidden(field.NewPath("spec", "sync", "target"), + "Another repository is already targeting root: "+v.Name) + } + } + } + + if len(all) >= 10 { + return field.Forbidden(field.NewPath("spec"), + "Maximum number of 10 repositories reached") + } + + return nil } func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartHookFunc, error) { postStartHooks := map[string]genericapiserver.PostStartHookFunc{ "grafana-provisioning": func(postStartHookCtx genericapiserver.PostStartHookContext) error { - // TODO: Set up a shared informer for a controller and a watcher with workers. + c, err := clientset.NewForConfig(postStartHookCtx.LoopbackClientConfig) + if err != nil { + return err + } + + // When starting with an empty instance -- swith to "mode 4+" + err = b.tryRunningOnlyUnifiedStorage() + if err != nil { + return err + } + + // Informer with resync interval used for health check and reconciliation + sharedInformerFactory := informers.NewSharedInformerFactory(c, 60*time.Second) + repoInformer := sharedInformerFactory.Provisioning().V0alpha1().Repositories() + go repoInformer.Informer().Run(postStartHookCtx.Context.Done()) + + // We do not have a local client until *GetPostStartHooks*, so we can delay init for some + b.tester = &RepositoryTester{ + client: c.ProvisioningV0alpha1(), + } + b.repositoryLister = repoInformer.Lister() + + exportWorker := export.NewExportWorker( + b.parsers.ClientFactory, + b.storageStatus, + b.secrets, + b.clonedir, + ) + syncWorker := sync.NewSyncWorker( + c.ProvisioningV0alpha1(), + b.parsers, + b.resourceLister, + b.storageStatus, + ) + migrationWorker := migrate.NewMigrationWorker( + b.legacyMigrator, + b.parsers, + b.storageStatus, + b.unified, + b.secrets, + exportWorker, + syncWorker, + b.clonedir, + ) + + // Pull request worker + renderer := pullrequest.NewScreenshotRenderer(b.render, b.unified, b.isPublic, b.urlProvider) + previewer := pullrequest.NewPreviewer(renderer, b.urlProvider) + pullRequestWorker, err := pullrequest.NewPullRequestWorker(b.parsers, previewer) + if err != nil { + return fmt.Errorf("create pull request worker: %w", err) + } + + driver := jobs.NewJobDriver(time.Second*28, time.Second*30, time.Second*30, b.jobs, b, + exportWorker, syncWorker, migrationWorker, pullRequestWorker) + go driver.Run(postStartHookCtx.Context) + + repoController, err := controller.NewRepositoryController( + c.ProvisioningV0alpha1(), + repoInformer, + b, // repoGetter + b.resourceLister, + b.parsers, + &repository.Tester{}, + b.jobs, + b.secrets, + b.storageStatus, + ) + if err != nil { + return err + } + + go repoController.Run(postStartHookCtx.Context, repoControllerWorkers) return nil }, } return postStartHooks, nil } +func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return provisioning.GetOpenAPIDefinitions +} + +// TODO: move endpoint specific logic to the connector so that we don't have things spread out all over the place. func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { oas.Info.Description = "Provisioning" root := "/apis/" + b.GetGroupVersion().String() + "/" + repoprefix := root + "namespaces/{namespace}/repositories/{name}" - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) + defsBase := "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1." + + sub := oas.Paths.Paths[repoprefix+"/test"] + if sub != nil { + repoSchema := defs[defsBase+"Repository"].Schema + sub.Post.Description = "Check if the configuration is valid" + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Required: false, + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &repoSchema, + }, + }, + }, + }, + } } + sub = oas.Paths.Paths[repoprefix+"/webhook"] + if sub != nil && sub.Get != nil { + sub.Post.Description = "Currently only supports github webhooks" + } + + ref := &spec3.Parameter{ + ParameterProps: spec3.ParameterProps{ + Name: "ref", + In: "query", + Example: "", + Examples: map[string]*spec3.Example{ + "": { + ExampleProps: spec3.ExampleProps{ + Summary: "The default", + }, + }, + "branch": { + ExampleProps: spec3.ExampleProps{ + Value: "my-branch", + Summary: "Select branch", + }, + }, + "commit": { + ExampleProps: spec3.ExampleProps{ + Value: "7f7cc2153", + Summary: "Commit hash (or prefix)", + }, + }, + }, + Description: "branch or commit hash", + Schema: spec.StringProperty(), + Required: false, + }, + } + + sub = oas.Paths.Paths[repoprefix+"/history"] + if sub != nil { + sub.Get.Description = "Get the history of the repository" + sub.Get.Parameters = []*spec3.Parameter{ref} + } + + sub = oas.Paths.Paths[repoprefix+"/history/{path}"] + if sub != nil { + sub.Get.Description = "Get the history of a path" + sub.Get.Parameters = []*spec3.Parameter{ref} + } + + // Show a special list command + sub = oas.Paths.Paths[repoprefix+"/files"] + if sub != nil { + delete(oas.Paths.Paths, repoprefix+"/files") + oas.Paths.Paths[repoprefix+"/files/"] = sub // add the trailing final slash + sub.Get.Description = "Get the files and content hash" + sub.Get.Summary = "File listing" + sub.Get.Parameters = []*spec3.Parameter{ref} + sub.Post = nil + sub.Put = nil + sub.Delete = nil + + // Replace the content type for this response + mt := sub.Get.Responses.StatusCodeResponses[200].Content + s := defs[defsBase+"FileList"].Schema + mt["*/*"].Schema = &s + } + + // update the version with a path + sub = oas.Paths.Paths[repoprefix+"/files/{path}"] + if sub != nil { + sub.Get.Description = "Read value from upstream repository" + sub.Get.Parameters = []*spec3.Parameter{ref} + + // Add message to the OpenAPI spec + comment := []*spec3.Parameter{ + ref, + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Description: "optional message sent with any changes", + Schema: spec.StringProperty(), + Required: false, + }, + }, + } + sub.Delete.Parameters = comment + sub.Post.Parameters = comment + sub.Put.Parameters = comment + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: spec.MapProperty(nil), + Example: &unstructured.Unstructured{}, + Examples: map[string]*spec3.Example{ + "dashboard": { + ExampleProps: spec3.ExampleProps{ + Value: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "hello": "dashboard", + }, + }, + }, + }, + }, + "playlist": { + ExampleProps: spec3.ExampleProps{ + Value: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "hello": "playlist", + }, + }, + }, + }, + }, + }, + }, + }, + "application/x-yaml": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: spec.MapProperty(nil), + Example: &unstructured.Unstructured{}, + Examples: map[string]*spec3.Example{ + "dashboard": { + ExampleProps: spec3.ExampleProps{ + Value: `apiVersion: dashboards.grafana.app/v0alpha1 +kind: Dashboard +spec: + title: Sample dashboard +`, + }, + }, + "playlist": { + ExampleProps: spec3.ExampleProps{ + Value: `apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +spec: + title: Playlist from provisioning + interval: 5m + items: + - type: dashboard_by_tag + value: panel-tests +`, + }, + }, + }, + }, + }, + }, + }, + } + // POST and put have the same request + sub.Put.RequestBody = sub.Post.RequestBody + } + + sub = oas.Paths.Paths[repoprefix+"/sync"] + if sub != nil { + optionsSchema := defs[defsBase+"SyncJobOptions"].Schema + sub.Post.Description = "Sync from repository into Grafana" + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &optionsSchema, + Example: &provisioning.SyncJobOptions{ + Incremental: false, + }, + }, + }, + }, + }, + } + } + + sub = oas.Paths.Paths[repoprefix+"/export"] + if sub != nil { + optionsSchema := defs[defsBase+"ExportJobOptions"].Schema + sub.Post.Description = "Export from grafana into the remote repository" + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &optionsSchema, + Example: &provisioning.ExportJobOptions{ + Folder: "grafan-folder-ref", + Branch: "target-branch", + Path: "path/in/tree", + }, + }, + }, + }, + }, + } + } + + sub = oas.Paths.Paths[repoprefix+"/migrate"] + if sub != nil { + optionsSchema := defs[defsBase+"MigrateJobOptions"].Schema + sub.Post.Description = "Export from grafana into the remote repository" + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &optionsSchema, + Example: &provisioning.MigrateJobOptions{ + History: true, + }, + }, + }, + }, + }, + } + } + + delete(oas.Paths.Paths, repoprefix+"/render") + sub = oas.Paths.Paths[repoprefix+"/render/{path}"] + if sub != nil { + sub.Get.Description = "get a rendered preview image" + sub.Get.Responses = &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "image/png": {}, + }, + Description: "OK", + }, + }, + }, + }, + } + } + + // Add any missing definitions + //----------------------------- + for k, v := range defs { + clean := strings.Replace(k, defsBase, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.", 1) + if oas.Components.Schemas[clean] == nil { + oas.Components.Schemas[clean] = &v.Schema + } + } + compBase := "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1." + schema := oas.Components.Schemas[compBase+"RepositoryViewList"].Properties["items"] + schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/" + compBase + "RepositoryView"), + }, + }, + }, + }, + }, + } + oas.Components.Schemas[compBase+"RepositoryViewList"].Properties["items"] = schema + + countSpec := &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/" + compBase + "ResourceCount"), + }, + }, + }, + }, + }, + } + managerSpec := &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/" + compBase + "ManagerStats"), + }, + }, + }, + }, + }, + } + schema = oas.Components.Schemas[compBase+"ResourceStats"].Properties["instance"] + schema.Items = countSpec + oas.Components.Schemas[compBase+"ResourceStats"].Properties["instance"] = schema + + schema = oas.Components.Schemas[compBase+"ResourceStats"].Properties["managed"] + schema.Items = managerSpec + oas.Components.Schemas[compBase+"ResourceStats"].Properties["managed"] = schema + + schema = oas.Components.Schemas[compBase+"ManagerStats"].Properties["stats"] + schema.Items = countSpec + oas.Components.Schemas[compBase+"ManagerStats"].Properties["stats"] = schema + return oas, nil } +// TODO: move this to a more appropriate place +func (b *APIBuilder) encryptSecrets(ctx context.Context, repo *provisioning.Repository) error { + var err error + if repo.Spec.GitHub != nil && + repo.Spec.GitHub.Token != "" { + repo.Spec.GitHub.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.GitHub.Token)) + if err != nil { + return err + } + repo.Spec.GitHub.Token = "" + } + + if repo.Status.Webhook != nil && + repo.Status.Webhook.Secret != "" { + repo.Status.Webhook.EncryptedSecret, err = b.secrets.Encrypt(ctx, []byte(repo.Status.Webhook.Secret)) + if err != nil { + return err + } + repo.Status.Webhook.Secret = "" + } + return nil +} + +// FIXME: This logic does not belong in provisioning! (but required for now) +// When starting an empty instance, we shift so that we never reference legacy storage +// This should run somewhere else at startup by default (dual writer? dashboards?) +func (b *APIBuilder) tryRunningOnlyUnifiedStorage() error { + ctx := context.Background() + + if !b.storageStatus.ShouldManage(dashboard.DashboardResourceInfo.GroupResource()) { + return nil // not enabled + } + + if !dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus) { + return nil + } + + // Count how many things exist + rsp, err := b.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + Namespace: "default", // FIXME! this works for single org, but need to check multi-org + Resources: []schema.GroupResource{{ + Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, + }, { + Group: folders.GROUP, Resource: folders.RESOURCE, + }}, + OnlyCount: true, + }) + if err != nil { + return fmt.Errorf("error getting legacy count %w", err) + } + for _, stats := range rsp.Summary { + if stats.Count > 0 { + return nil // something exists we can not just switch + } + } + + logger := logging.DefaultLogger.With("logger", "provisioning startup") + mode5 := func(gr schema.GroupResource) error { + status, _ := b.storageStatus.Status(ctx, gr) + if !status.ReadUnified { + status.ReadUnified = true + status.WriteLegacy = false + status.WriteUnified = true + status.Runtime = false + status.Migrated = time.Now().UnixMilli() + _, err = b.storageStatus.Update(ctx, status) + logger.Info("set unified storage access", "group", gr.Group, "resource", gr.Resource) + return err + } + return nil // already reading unified + } + + if err = mode5(dashboard.DashboardResourceInfo.GroupResource()); err != nil { + return err + } + if err = mode5(folders.FolderResourceInfo.GroupResource()); err != nil { + return err + } + return nil +} + // Helpers for fetching valid Repository objects +// TODO: where should the helpers live? func (b *APIBuilder) GetRepository(ctx context.Context, name string) (repository.Repository, error) { obj, err := b.getter.Get(ctx, name, &metav1.GetOptions{}) if err != nil { return nil, err } + return b.asRepository(ctx, obj) +} - _ = obj - // FIXME: Return a valid Repository object with the correct underlying storage. - panic("FIXME") +func timeSince(when int64) time.Duration { + return time.Duration(time.Now().UnixMilli()-when) * time.Millisecond +} + +func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (repository.Repository, error) { + repo, err := b.GetRepository(ctx, name) + if err != nil { + return nil, err + } + status := repo.Config().Status.Health + if !status.Healthy { + if timeSince(status.Checked) > time.Second*25 { + ctx, _, err = identity.WithProvisioningIdentity(ctx, repo.Config().Namespace) + if err != nil { + return nil, err // The status + } + + // Check health again + s, err := repository.TestRepository(ctx, repo) + if err != nil { + return nil, err // The status + } + + // Write and return the repo with current status + cfg, _ := b.tester.UpdateHealthStatus(ctx, repo.Config(), s) + if cfg != nil { + status = cfg.Status.Health + if cfg.Status.Health.Healthy { + status = cfg.Status.Health + repo, err = b.AsRepository(ctx, cfg) + if err != nil { + return nil, err + } + } + } + } + if !status.Healthy { + return nil, &apierrors.StatusError{ErrStatus: metav1.Status{ + Code: http.StatusFailedDependency, + Message: "The repository configuration is not healthy", + }} + } + } + return repo, err +} + +func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repository.Repository, error) { + if obj == nil { + return nil, fmt.Errorf("missing repository object") + } + r, ok := obj.(*provisioning.Repository) + if !ok { + return nil, fmt.Errorf("expected repository configuration") + } + return b.AsRepository(ctx, r) +} + +func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) { + switch r.Spec.Type { + case provisioning.LocalRepositoryType: + return repository.NewLocal(r, b.localFileResolver), nil + case provisioning.GitHubRepositoryType: + gvr := provisioning.RepositoryResourceInfo.GroupVersionResource() + var webhookURL string + if b.isPublic { + webhookURL = fmt.Sprintf( + "%sapis/%s/%s/namespaces/%s/%s/%s/webhook", + b.urlProvider(r.GetNamespace()), + gvr.Group, + gvr.Version, + r.GetNamespace(), + gvr.Resource, + r.GetName(), + ) + } + return repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, webhookURL) + default: + return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type) + } } diff --git a/pkg/registry/apis/provisioning/render.go b/pkg/registry/apis/provisioning/render.go new file mode 100644 index 00000000000..3bfc587364c --- /dev/null +++ b/pkg/registry/apis/provisioning/render.go @@ -0,0 +1,131 @@ +package provisioning + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type renderConnector struct { + blob resource.BlobStoreClient +} + +func (*renderConnector) New() runtime.Object { + return &provisioning.Repository{} +} + +func (*renderConnector) Destroy() {} + +func (*renderConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (c *renderConnector) ProducesObject(verb string) any { + return c.New() +} + +func (*renderConnector) ConnectMethods() []string { + return []string{http.MethodGet} +} + +func (*renderConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, true, "" +} + +func (c *renderConnector) Connect( + ctx context.Context, + name string, + opts runtime.Object, + responder rest.Responder, +) (http.Handler, error) { + namespace := request.NamespaceValue(ctx) + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + prefix := fmt.Sprintf("/%s/render", name) + idx := strings.Index(r.URL.Path, prefix) + if idx == -1 { + logger.Debug("failed to find a file path in the URL") + responder.Error(apierrors.NewBadRequest("invalid request path")) + return + } + blobID := strings.TrimPrefix(r.URL.Path[idx+len(prefix):], "/") + if len(blobID) == 0 { + responder.Error(apierrors.NewNotFound(provisioning.RepositoryResourceInfo.GroupResource(), "render")) + return + } + if !validBlobID(blobID) { + responder.Error(apierrors.NewBadRequest(fmt.Sprintf("invalid blob id: %s", blobID))) + return + } + + rsp, err := c.blob.GetBlob(ctx, &resource.GetBlobRequest{ + Resource: &resource.ResourceKey{ + Namespace: namespace, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GroupResource().Resource, + Name: name, + }, + MustProxyBytes: true, + Uid: blobID, + }) + if err != nil { + responder.Error(err) + return + } + if rsp.Error != nil { + responder.Error(resource.GetError(rsp.Error)) + return + } + + if len(rsp.Value) > 0 { + if rsp.ContentType != "" { + w.Header().Add("Content-Type", rsp.ContentType) + } + _, err = w.Write(rsp.Value) + if err != nil { + responder.Error(err) + return + } + } else { + responder.Error(&apierrors.StatusError{ + ErrStatus: v1.Status{ + Code: http.StatusNoContent, + Message: "empty body", + }, + }) + } + }), 20*time.Second), nil +} + +// validBlobID ensures the ID is valid for a blob. +// The ID is always a UUID. As such, this checks for something that can resemble a UUID. +// This does not check for the ID to be an actual UUID, as the blob store may change their ID format, which we do not wish to stand in the way of. +func validBlobID(id string) bool { + for _, c := range id { + // [a-zA-Z0-9\-] are valid characters. + az := c >= 'a' && c <= 'z' + AZ := c >= 'A' && c <= 'Z' + digit := c >= '0' && c <= '9' + if !(az || AZ || digit || c == '-') { + return false + } + } + return true +} + +var ( + _ rest.Connecter = (*renderConnector)(nil) + _ rest.Storage = (*renderConnector)(nil) + _ rest.StorageMetadata = (*renderConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/repository/context.go b/pkg/registry/apis/provisioning/repository/context.go new file mode 100644 index 00000000000..ac10239a130 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/context.go @@ -0,0 +1,31 @@ +package repository + +import ( + "context" + "time" +) + +// When writing values from history, we want the metadata to match Legacy grafana SQL +type CommitSignature struct { + // Name represents a person name. It is an arbitrary string. + Name string + // Email is an email, but it cannot be assumed to be well-formed. + Email string + // When is the timestamp of the signature. + When time.Time +} + +type ctxAuthorKey struct{} + +func WithAuthorSignature(ctx context.Context, sig CommitSignature) context.Context { + return context.WithValue(ctx, ctxAuthorKey{}, sig) +} + +func GetAuthorSignature(ctx context.Context) *CommitSignature { + u, ok := ctx.Value(ctxAuthorKey{}).(CommitSignature) + if ok { + copy := u + return © + } + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/context_test.go b/pkg/registry/apis/provisioning/repository/context_test.go new file mode 100644 index 00000000000..2fd49c33b15 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/context_test.go @@ -0,0 +1,52 @@ +package repository + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestAuthorSignature(t *testing.T) { + t.Run("should store and retrieve author signature", func(t *testing.T) { + expected := CommitSignature{ + Name: "John Doe", + Email: "john@example.com", + When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + } + + ctx := context.Background() + ctx = WithAuthorSignature(ctx, expected) + + result := GetAuthorSignature(ctx) + require.NotNil(t, result) + require.Equal(t, expected.Name, result.Name) + require.Equal(t, expected.Email, result.Email) + require.Equal(t, expected.When, result.When) + }) + + t.Run("should return nil when no signature is set", func(t *testing.T) { + ctx := context.Background() + result := GetAuthorSignature(ctx) + require.Nil(t, result) + }) + + t.Run("should return copy of signature", func(t *testing.T) { + original := CommitSignature{ + Name: "John Doe", + Email: "john@example.com", + When: time.Now(), + } + + ctx := context.Background() + ctx = WithAuthorSignature(ctx, original) + + result1 := GetAuthorSignature(ctx) + result2 := GetAuthorSignature(ctx) + + require.NotNil(t, result1) + require.NotNil(t, result2) + require.NotSame(t, result1, result2) + }) +} diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go new file mode 100644 index 00000000000..21d8903ef35 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -0,0 +1,916 @@ +package repository + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "net/url" + "regexp" + "slices" + "strings" + + "github.com/google/go-github/v69/github" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/google/uuid" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +var subscribedEvents = []string{"push", "pull_request"} + +// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. +type githubRepository struct { + config *provisioning.Repository + gh pgh.Client // assumes github.com base URL + secrets secrets.Service + webhookURL string + + owner string + repo string +} + +var ( + _ Repository = (*githubRepository)(nil) + _ Hooks = (*githubRepository)(nil) + _ Versioned = (*githubRepository)(nil) + _ Writer = (*githubRepository)(nil) + _ Reader = (*githubRepository)(nil) + _ RepositoryWithURLs = (*githubRepository)(nil) +) + +func NewGitHub( + ctx context.Context, + config *provisioning.Repository, + factory *pgh.Factory, + secrets secrets.Service, + webhookURL string, +) (*githubRepository, error) { + owner, repo, err := parseOwnerRepo(config.Spec.GitHub.URL) + if err != nil { + return nil, err + } + token := config.Spec.GitHub.Token + if token == "" { + decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) + if err != nil { + return nil, err + } + token = string(decrypted) + } + return &githubRepository{ + config: config, + gh: factory.New(ctx, token), // TODO, baseURL from config + secrets: secrets, + webhookURL: webhookURL, + owner: owner, + repo: repo, + }, nil +} + +func (r *githubRepository) Config() *provisioning.Repository { + return r.config +} + +// Validate implements provisioning.Repository. +func (r *githubRepository) Validate() (list field.ErrorList) { + gh := r.config.Spec.GitHub + if gh == nil { + list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) + return list + } + if gh.URL == "" { + list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) + } else { + _, _, err := parseOwnerRepo(gh.URL) + if err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) + } else if !strings.HasPrefix(gh.URL, "https://github.com/") { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) + } + } + if gh.Branch == "" { + list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required")) + } + if !isValidGitBranchName(gh.Branch) { + list = append(list, field.Invalid(field.NewPath("spec", "github", "branch"), gh.Branch, "invalid branch name")) + } + // TODO: Use two fields for token + if gh.Token == "" && len(gh.EncryptedToken) == 0 { + list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required")) + } + + if err := safepath.IsSafe(gh.Path); err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, err.Error())) + } + + if safepath.IsAbs(gh.Path) { + list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, "path must be relative")) + } + + return list +} + +func parseOwnerRepo(giturl string) (owner string, repo string, err error) { + parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git")) + if e != nil { + err = e + return + } + parts := strings.Split(parsed.Path, "/") + if len(parts) < 3 { + err = fmt.Errorf("unable to parse repo+owner from url") + return + } + return parts[1], parts[2], nil +} + +func fromError(err error, code int) *provisioning.TestResults { + statusErr, ok := err.(apierrors.APIStatus) + if ok { + s := statusErr.Status() + return &provisioning.TestResults{ + Code: int(s.Code), + Success: false, + Errors: []string{s.Message}, + } + } + return &provisioning.TestResults{ + Code: code, + Success: false, + Errors: []string{err.Error()}, + } +} + +// Test implements provisioning.Repository. +func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + if err := r.gh.IsAuthenticated(ctx); err != nil { + return fromError(err, http.StatusUnauthorized), nil + } + + owner, repo, err := parseOwnerRepo(r.config.Spec.GitHub.URL) + if err != nil { + return fromError(err, http.StatusBadRequest), nil + } + + // FIXME: check token permissions + ok, err := r.gh.RepoExists(ctx, owner, repo) + if err != nil { + return fromError(err, http.StatusBadRequest), nil + } + + if !ok { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{"repository does not exist"}, + }, nil + } + + ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, r.config.Spec.GitHub.Branch) + if err != nil { + return fromError(err, http.StatusBadRequest), nil + } + + if !ok { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{"branch does not exist"}, + }, nil + } + + return &provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil +} + +// ReadResource implements provisioning.Repository. +func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*FileInfo, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, filePath) + content, dirContent, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: fmt.Sprintf("file not found; path=%s ref=%s", finalPath, ref), + Code: http.StatusNotFound, + }, + } + } + + return nil, fmt.Errorf("get contents: %w", err) + } + if dirContent != nil { + return &FileInfo{ + Path: filePath, + Ref: ref, + }, nil + } + + data, err := content.GetFileContent() + if err != nil { + return nil, fmt.Errorf("get content: %w", err) + } + return &FileInfo{ + Path: filePath, + Ref: ref, + Data: []byte(data), + Hash: content.GetSHA(), + }, nil +} + +func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + + ctx, logger := r.logger(ctx, ref) + + tree, truncated, err := r.gh.GetTree(ctx, r.owner, r.repo, r.config.Spec.GitHub.Path, ref, true) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: fmt.Sprintf("tree not found; ref=%s", ref), + Code: http.StatusNotFound, + }, + } + } + } + if truncated { + logger.Warn("tree from github was truncated") + } + + entries := make([]FileTreeEntry, 0, len(tree)) + for _, entry := range tree { + converted := FileTreeEntry{ + Path: entry.GetPath(), + Size: entry.GetSize(), + Hash: entry.GetSHA(), + Blob: !entry.IsDirectory(), + } + entries = append(entries, converted) + } + return entries, nil +} + +func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + ctx, _ = r.logger(ctx, ref) + + if err := r.ensureBranchExists(ctx, ref); err != nil { + return fmt.Errorf("create branch on create: %w", err) + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + + // Create .keep file if it is a directory + if safepath.IsDir(finalPath) { + if data != nil { + return apierrors.NewBadRequest("data cannot be provided for a directory") + } + + finalPath = safepath.Join(finalPath, ".keep") + data = []byte{} + } + + err := r.gh.CreateFile(ctx, r.owner, r.repo, finalPath, ref, comment, data) + if errors.Is(err, pgh.ErrResourceAlreadyExists) { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "file already exists", + Code: http.StatusConflict, + }, + } + } + + return err +} + +func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + ctx, _ = r.logger(ctx, ref) + + if err := r.ensureBranchExists(ctx, ref); err != nil { + return fmt.Errorf("create branch on update: %w", err) + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + file, _, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "file not found", + Code: http.StatusNotFound, + }, + } + } + + return fmt.Errorf("get content before file update: %w", err) + } + if file.IsDirectory() { + return apierrors.NewBadRequest("cannot update a directory") + } + + if err := r.gh.UpdateFile(ctx, r.owner, r.repo, finalPath, ref, comment, file.GetSHA(), data); err != nil { + return fmt.Errorf("update file: %w", err) + } + return nil +} + +func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + ctx, _ = r.logger(ctx, ref) + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + + return writeWithReadThenCreateOrUpdate(ctx, r, finalPath, ref, data, message) +} + +func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + ctx, _ = r.logger(ctx, ref) + + if err := r.ensureBranchExists(ctx, ref); err != nil { + return fmt.Errorf("create branch on delete: %w", err) + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + + return r.deleteRecursively(ctx, finalPath, ref, comment) +} + +func (r *githubRepository) deleteRecursively(ctx context.Context, path, ref, comment string) error { + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + file, contents, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "file not found", + Code: http.StatusNotFound, + }, + } + } + return fmt.Errorf("finding file to delete: %w", err) + } + + if file != nil && !file.IsDirectory() { + return r.gh.DeleteFile(ctx, r.owner, r.repo, finalPath, ref, comment, file.GetSHA()) + } + + for _, c := range contents { + if c.IsDirectory() { + if err := r.deleteRecursively(ctx, c.GetPath(), ref, comment); err != nil { + return fmt.Errorf("delete file recursive: %w", err) + } + continue + } + + if err := r.gh.DeleteFile(ctx, r.owner, r.repo, c.GetPath(), ref, comment, c.GetSHA()); err != nil { + return fmt.Errorf("delete file: %w", err) + } + } + + return nil +} + +func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + ctx, _ = r.logger(ctx, ref) + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "path not found", + Code: http.StatusNotFound, + }, + } + } + + return nil, fmt.Errorf("get commits: %w", err) + } + + ret := make([]provisioning.HistoryItem, 0, len(commits)) + for _, commit := range commits { + authors := make([]provisioning.Author, 0) + if commit.Author != nil { + authors = append(authors, provisioning.Author{ + Name: commit.Author.Name, + Username: commit.Author.Username, + AvatarURL: commit.Author.AvatarURL, + }) + } + + if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name { + authors = append(authors, provisioning.Author{ + Name: commit.Committer.Name, + Username: commit.Committer.Username, + AvatarURL: commit.Committer.AvatarURL, + }) + } + + ret = append(ret, provisioning.HistoryItem{ + Ref: commit.Ref, + Message: commit.Message, + Authors: authors, + CreatedAt: commit.CreatedAt.UnixMilli(), + }) + } + + return ret, nil +} + +// basicGitBranchNameRegex is a regular expression to validate a git branch name +// it does not cover all cases as positive lookaheads are not supported in Go's regexp +var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`) + +// isValidGitBranchName checks if a branch name is valid. +// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules: +// 1. The branch name must have at least one character and must not be empty. +// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace. +// 3. The branch name cannot contain consecutive slashes (`//`). +// 4. The branch name cannot contain consecutive dots (`..`). +// 5. The branch name cannot contain `@{`. +// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`. +func isValidGitBranchName(branch string) bool { + if !basicGitBranchNameRegex.MatchString(branch) { + return false + } + + // Additional checks for invalid patterns + if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") || + strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") || + strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") { + return false + } + + return true +} + +func (r *githubRepository) ensureBranchExists(ctx context.Context, branchName string) error { + if !isValidGitBranchName(branchName) { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusBadRequest, + Message: "invalid branch name", + }, + } + } + + ok, err := r.gh.BranchExists(ctx, r.owner, r.repo, branchName) + if err != nil { + return fmt.Errorf("check branch exists: %w", err) + } + + if ok { + logging.FromContext(ctx).Info("branch already exists", "branch", branchName) + + return nil + } + + srcBranch := r.config.Spec.GitHub.Branch + if err := r.gh.CreateBranch(ctx, r.owner, r.repo, srcBranch, branchName); err != nil { + if errors.Is(err, pgh.ErrResourceAlreadyExists) { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusConflict, + Message: "branch already exists", + }, + } + } + + return fmt.Errorf("create branch: %w", err) + } + + return nil +} + +// Webhook implements Repository. +func (r *githubRepository) Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error) { + if r.config.Status.Webhook == nil { + return nil, fmt.Errorf("unexpected webhook request") + } + + secret, err := r.secrets.Decrypt(ctx, r.config.Status.Webhook.EncryptedSecret) + if err != nil { + return nil, fmt.Errorf("failed to decrypt secret: %w", err) + } + + payload, err := github.ValidatePayload(req, secret) + if err != nil { + return nil, apierrors.NewUnauthorized("invalid signature") + } + + return r.parseWebhook(github.WebHookType(req), payload) +} + +// This method does not include context because it does delegate any more requests +func (r *githubRepository) parseWebhook(messageType string, payload []byte) (*provisioning.WebhookResponse, error) { + event, err := github.ParseWebHook(messageType, payload) + if err != nil { + return nil, apierrors.NewBadRequest("invalid payload") + } + + switch event := event.(type) { + case *github.PushEvent: + return r.parsePushEvent(event) + case *github.PullRequestEvent: + return r.parsePullRequestEvent(event) + case *github.PingEvent: + return &provisioning.WebhookResponse{ + Code: http.StatusOK, + Message: "ping received", + }, nil + } + + return &provisioning.WebhookResponse{ + Code: http.StatusNotImplemented, + Message: fmt.Sprintf("unsupported messageType: %s", messageType), + }, nil +} + +func (r *githubRepository) parsePushEvent(event *github.PushEvent) (*provisioning.WebhookResponse, error) { + if event.GetRepo() == nil { + return nil, fmt.Errorf("missing repository in push event") + } + if event.GetRepo().GetFullName() != fmt.Sprintf("%s/%s", r.owner, r.repo) { + return nil, fmt.Errorf("repository mismatch") + } + + // No need to sync if not enabled + if !r.config.Spec.Sync.Enabled { + return &provisioning.WebhookResponse{Code: http.StatusOK}, nil + } + + // Skip silently if the event is not for the main/master branch + // as we cannot configure the webhook to only publish events for the main branch + if event.GetRef() != fmt.Sprintf("refs/heads/%s", r.config.Spec.GitHub.Branch) { + return &provisioning.WebhookResponse{Code: http.StatusOK}, nil + } + + return &provisioning.WebhookResponse{ + Code: http.StatusAccepted, + Job: &provisioning.JobSpec{ + Repository: r.Config().GetName(), + Action: provisioning.JobActionSync, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }, nil +} + +func (r *githubRepository) parsePullRequestEvent(event *github.PullRequestEvent) (*provisioning.WebhookResponse, error) { + if event.GetRepo() == nil { + return nil, fmt.Errorf("missing repository in pull request event") + } + cfg := r.config.Spec.GitHub + if cfg == nil { + return nil, fmt.Errorf("missing github config") + } + + if event.GetRepo().GetFullName() != fmt.Sprintf("%s/%s", r.owner, r.repo) { + return nil, fmt.Errorf("repository mismatch") + } + pr := event.GetPullRequest() + if pr == nil { + return nil, fmt.Errorf("expected PR in event") + } + + if pr.GetBase().GetRef() != r.config.Spec.GitHub.Branch { + return &provisioning.WebhookResponse{ + Code: http.StatusOK, + Message: fmt.Sprintf("ignoring pull request event as %s is not the configured branch", pr.GetBase().GetRef()), + }, nil + } + + action := event.GetAction() + if action != "opened" && action != "reopened" && action != "synchronize" { + return &provisioning.WebhookResponse{ + Code: http.StatusOK, // Nothing needed + Message: fmt.Sprintf("ignore pull request event: %s", action), + }, nil + } + + // Queue an async job that will parse files + return &provisioning.WebhookResponse{ + Code: http.StatusAccepted, // Nothing needed + Message: fmt.Sprintf("pull request: %s", action), + Job: &provisioning.JobSpec{ + Repository: r.Config().GetName(), + Action: provisioning.JobActionPullRequest, + PullRequest: &provisioning.PullRequestJobOptions{ + URL: pr.GetHTMLURL(), + PR: pr.GetNumber(), + Ref: pr.GetHead().GetRef(), + Hash: pr.GetHead().GetSHA(), + }, + }, + }, nil +} + +func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { + ctx, _ = r.logger(ctx, "") + branch, err := r.gh.GetBranch(ctx, r.owner, r.repo, r.Config().Spec.GitHub.Branch) + if err != nil { + return "", fmt.Errorf("get branch: %w", err) + } + + return branch.Sha, nil +} + +func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) { + if ref == "" { + var err error + ref, err = r.LatestRef(ctx) + if err != nil { + return nil, fmt.Errorf("get latest ref: %w", err) + } + } + ctx, logger := r.logger(ctx, ref) + + files, err := r.gh.CompareCommits(ctx, r.owner, r.repo, base, ref) + if err != nil { + return nil, fmt.Errorf("compare commits: %w", err) + } + + changes := make([]VersionedFileChange, 0) + for _, f := range files { + // reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit + switch f.GetStatus() { + case "added", "copied": + changes = append(changes, VersionedFileChange{ + Path: f.GetFilename(), + Ref: ref, + Action: FileActionCreated, + }) + case "modified", "changed": + changes = append(changes, VersionedFileChange{ + Path: f.GetFilename(), + Ref: ref, + Action: FileActionUpdated, + }) + case "renamed": + changes = append(changes, VersionedFileChange{ + Path: f.GetFilename(), + PreviousPath: f.GetPreviousFilename(), + Ref: ref, + PreviousRef: base, + Action: FileActionRenamed, + }) + case "removed": + changes = append(changes, VersionedFileChange{ + Ref: base, + Path: f.GetFilename(), + Action: FileActionDeleted, + }) + case "unchanged": + // do nothing + default: + logger.Error("ignore unhandled file", "file", f.GetFilename(), "status", f.GetStatus()) + } + } + + return changes, nil +} + +// ClearAllPullRequestFileComments clears all comments on a pull request +func (r *githubRepository) ClearAllPullRequestFileComments(ctx context.Context, prNumber int) error { + ctx, _ = r.logger(ctx, "") + return r.gh.ClearAllPullRequestFileComments(ctx, r.owner, r.repo, prNumber) +} + +// CommentPullRequest adds a comment to a pull request. +func (r *githubRepository) CommentPullRequest(ctx context.Context, prNumber int, comment string) error { + ctx, _ = r.logger(ctx, "") + return r.gh.CreatePullRequestComment(ctx, r.owner, r.repo, prNumber, comment) +} + +// CommentPullRequestFile lints a file and comments the issues found. +func (r *githubRepository) CommentPullRequestFile(ctx context.Context, prNumber int, path, ref, comment string) error { + ctx, _ = r.logger(ctx, ref) + fileComment := pgh.FileComment{ + Content: comment, + Path: path, + Position: 1, // create a top-level comment + Ref: ref, + } + + // FIXME: comment with Grafana Logo + // FIXME: comment author should be written by Grafana and not the user + return r.gh.CreatePullRequestFileComment(ctx, r.owner, r.repo, prNumber, fileComment) +} + +// ResourceURLs implements RepositoryWithURLs. +func (r *githubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) { + cfg := r.config.Spec.GitHub + if file.Path == "" || cfg == nil { + return nil, nil + } + + ref := file.Ref + if ref == "" { + ref = cfg.Branch + } + + urls := &provisioning.ResourceURLs{ + RepositoryURL: cfg.URL, + SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), + } + + if ref != cfg.Branch { + urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) + + // Create a new pull request + urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) + } + + return urls, nil +} + +func (r *githubRepository) createWebhook(ctx context.Context) (pgh.WebhookConfig, error) { + secret, err := uuid.NewRandom() + if err != nil { + return pgh.WebhookConfig{}, fmt.Errorf("could not generate secret: %w", err) + } + + cfg := pgh.WebhookConfig{ + URL: r.webhookURL, + Secret: secret.String(), + ContentType: "json", + Events: subscribedEvents, + Active: true, + } + + hook, err := r.gh.CreateWebhook(ctx, r.owner, r.repo, cfg) + if err != nil { + return pgh.WebhookConfig{}, err + } + + // HACK: GitHub does not return the secret, so we need to update it manually + hook.Secret = cfg.Secret + + logging.FromContext(ctx).Info("webhook created", "url", cfg.URL, "id", hook.ID) + return hook, nil +} + +// updateWebhook checks if the webhook needs to be updated and updates it if necessary. +// if the webhook does not exist, it will create it. +func (r *githubRepository) updateWebhook(ctx context.Context) (pgh.WebhookConfig, bool, error) { + if r.config.Status.Webhook == nil || r.config.Status.Webhook.ID == 0 { + hook, err := r.createWebhook(ctx) + if err != nil { + return pgh.WebhookConfig{}, false, err + } + return hook, true, nil + } + + hook, err := r.gh.GetWebhook(ctx, r.owner, r.repo, r.config.Status.Webhook.ID) + switch { + case errors.Is(err, pgh.ErrResourceNotFound): + hook, err := r.createWebhook(ctx) + if err != nil { + return pgh.WebhookConfig{}, false, err + } + return hook, true, nil + case err != nil: + return pgh.WebhookConfig{}, false, fmt.Errorf("get webhook: %w", err) + } + + hook.Secret = r.config.Status.Webhook.Secret // we always random gen this, so don't use it for mustUpdate below. + + var mustUpdate bool + + if hook.URL != r.config.Status.Webhook.URL { + mustUpdate = true + hook.URL = r.webhookURL + } + + if !slices.Equal(hook.Events, subscribedEvents) { + mustUpdate = true + hook.Events = subscribedEvents + } + + if !mustUpdate { + return hook, false, nil + } + + // Something has changed in the webhook. Let's rotate the secret as well, so as to ensure we end up with a 100% correct webhook. + secret, err := uuid.NewRandom() + if err != nil { + return pgh.WebhookConfig{}, false, fmt.Errorf("could not generate secret: %w", err) + } + hook.Secret = secret.String() + + if err := r.gh.EditWebhook(ctx, r.owner, r.repo, hook); err != nil { + return pgh.WebhookConfig{}, false, fmt.Errorf("edit webhook: %w", err) + } + + return hook, true, nil +} + +func (r *githubRepository) deleteWebhook(ctx context.Context) error { + if r.config.Status.Webhook == nil { + return fmt.Errorf("webhook not found") + } + + id := r.config.Status.Webhook.ID + + if err := r.gh.DeleteWebhook(ctx, r.owner, r.repo, id); err != nil { + return fmt.Errorf("delete webhook: %w", err) + } + + logging.FromContext(ctx).Info("webhook deleted", "url", r.config.Status.Webhook.URL, "id", id) + return nil +} + +func (r *githubRepository) OnCreate(ctx context.Context) (*provisioning.WebhookStatus, error) { + if len(r.webhookURL) == 0 { + return nil, nil + } + + ctx, _ = r.logger(ctx, "") + hook, err := r.createWebhook(ctx) + if err != nil { + return nil, err + } + return &provisioning.WebhookStatus{ + ID: hook.ID, + URL: hook.URL, + Secret: hook.Secret, + SubscribedEvents: hook.Events, + }, nil +} + +func (r *githubRepository) OnUpdate(ctx context.Context) (*provisioning.WebhookStatus, error) { + if len(r.webhookURL) == 0 { + return nil, nil + } + ctx, _ = r.logger(ctx, "") + hook, _, err := r.updateWebhook(ctx) + if err != nil { + return nil, err + } + + return &provisioning.WebhookStatus{ + ID: hook.ID, + URL: hook.URL, + Secret: hook.Secret, + SubscribedEvents: hook.Events, + }, nil +} + +func (r *githubRepository) OnDelete(ctx context.Context) error { + if len(r.webhookURL) == 0 { + return nil + } + ctx, _ = r.logger(ctx, "") + return r.deleteWebhook(ctx) +} + +func (r *githubRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) { + logger := logging.FromContext(ctx) + + type containsGh int + var containsGhKey containsGh + if ctx.Value(containsGhKey) != nil { + return ctx, logging.FromContext(ctx) + } + + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + logger = logger.With(slog.Group("github_repository", "owner", r.owner, "name", r.repo, "ref", ref)) + ctx = logging.Context(ctx, logger) + // We want to ensure we don't add multiple github_repository keys. With doesn't deduplicate the keys... + ctx = context.WithValue(ctx, containsGhKey, true) + return ctx, logger +} diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go new file mode 100644 index 00000000000..17669d73b77 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -0,0 +1,182 @@ +// The github package exists to provide a client for the GH API, which can also be faked with a mock. +// In most cases, we want the real client, but testing should mock it, lest we get blocked from their API, or have to configure auth for simple tests. +package github + +import ( + "context" + "errors" + "time" + + "github.com/google/go-github/v69/github" + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +// API errors that we need to convey after parsing real GH errors (or faking them). +var ( + ErrResourceAlreadyExists = errors.New("the resource already exists") + ErrResourceNotFound = errors.New("the resource does not exist") + ErrMismatchedHash = errors.New("the update cannot be applied because the expected and actual hashes are unequal") + ErrNoSecret = errors.New("new webhooks must have a secret") + //lint:ignore ST1005 this is not punctuation + ErrPathTraversalDisallowed = errors.New("the path contained ..") //nolint:stylecheck + ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") + ErrFileTooLarge = errors.New("file exceeds maximum allowed size") + ErrTooManyItems = errors.New("maximum number of items exceeded") +) + +// MaxFileSize maximum file size limit (10MB) +const MaxFileSize = 10 * 1024 * 1024 // 10MB in bytes + +type ErrRateLimited = github.RateLimitError + +type Client interface { + // IsAuthenticated checks if the client is authenticated. + IsAuthenticated(ctx context.Context) error + + // GetContents returns the metadata and content of a file or directory. + // When a file is checked, the first returned value will have a value. For a directory, the second will. The other value is always nil. + // If an error occurs, the returned values may or may not be nil. + // + // If ".." appears in the "path", this method will return an error. + GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) + + // GetTree returns the Git tree in the repository. + // When recursive is given, subtrees are mapped into the returned array. + // When basePath is given, only trees under it are given. The results do not include this path in their names. + // + // The truncated bool will be set to true if the tree is larger than 7 MB or 100 000 entries. + // When truncated is true, you may wish to read each subtree manually instead. + GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) (entries []RepositoryContent, truncated bool, err error) + + // CreateFile creates a new file in the repository under the given path. + // The file is created on the branch given. + // The message given is the commit message. If none is given, an appropriate default is used. + // The content is what the file should contain. An empty slice is valid, though often not very useful. + // + // If ".." appears in the "path", this method will return an error. + CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error + + // UpdateFile updates a file in the repository under the given path. + // The file is updated on the branch given. + // The message given is the commit message. If none is given, an appropriate default is used. + // The content is what the file should contain. An empty slice is valid, though often not very useful. + // If the path does not exist, an error is returned. + // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. + // + // If ".." appears in the "path", this method will return an error. + UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error + + // DeleteFile deletes a file in the repository under the given path. + // The file is deleted from the branch given. + // The message given is the commit message. If none is given, an appropriate default is used. + // If the path does not exist, an error is returned. + // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. + // + // If ".." appears in the "path", this method will return an error. + DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error + + // Commits returns the commits for the given path + Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) + + // CompareCommits returns the changes between two commits. + CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) + + // RepoExists checks if a repository exists. + RepoExists(ctx context.Context, owner, repository string) (bool, error) + + // CreateBranch creates a new branch in the repository. + CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error + // BranchExists checks if a branch exists in the repository. + BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) + // GetBranch returns the branch of the repository. + GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) + + ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) + CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) + GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) + DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error + EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error + + ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) + CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error + CreatePullRequestFileComment(ctx context.Context, owner, repository string, number int, comment FileComment) error + ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error +} + +type RepositoryContent interface { + // Returns true if this is a directory, false if it is a file. + IsDirectory() bool + // Returns the contents of the file. Decoding happens if necessary. + // Returns an error if the content represents a directory. + GetFileContent() (string, error) + // Returns true if this is a symlink. + // If true, GetPath returns the path where this symlink leads. + IsSymlink() bool + // Returns the full path from the root of the repository. + // This has no leading or trailing slashes. + // The path only uses '/' for directories. You can use the 'path' package to interact with these. + GetPath() string + // Get the SHA hash. This is usually a SHA-256, but may also be SHA-512. + // Directories have SHA hashes, too (TODO: how is this calculated?). + GetSHA() string + // The size of the file. Not necessarily non-zero, even if the file is supposed to be non-zero. + GetSize() int64 +} + +type Branch struct { + Name string + Sha string +} + +type CommitAuthor struct { + Name string + Username string + AvatarURL string +} + +type Commit struct { + Ref string + Message string + Author *CommitAuthor + Committer *CommitAuthor + CreatedAt time.Time +} + +type CommitFile interface { + GetSHA() string + GetFilename() string + GetPreviousFilename() string + GetStatus() string +} + +type FileComment struct { + Content string + Path string + Position int + Ref string +} + +type CreateFileOptions struct { + // The message of the commit. May be empty, in which case a default value is entered. + Message string + // The content of the file to write, unencoded. + Content []byte +} + +type WebhookConfig struct { + // The ID of the webhook. + // Can be 0 on creation. + ID int64 + // The events which this webhook shall contact the URL for. + Events []string + // Is the webhook enabled? + Active bool + // The URL GitHub should contact on events. + URL string + // The content type GitHub should send to the URL. + // If not specified, this is "form". + ContentType string + // The secret to use when sending events to the URL. + // If fetched from GitHub, this is empty as it contains no useful information. + Secret string +} diff --git a/pkg/registry/apis/provisioning/repository/github/factory.go b/pkg/registry/apis/provisioning/repository/github/factory.go new file mode 100644 index 00000000000..aea36ce8dcd --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/factory.go @@ -0,0 +1,32 @@ +package github + +import ( + "context" + "net/http" + + "github.com/google/go-github/v69/github" + "golang.org/x/oauth2" +) + +// Factory creates new GitHub clients. +// It exists only for the ability to test the code easily. +type Factory struct { + // Client allows overriding the client to use in the GH client returned. It exists primarily for testing. + Client *http.Client +} + +func ProvideFactory() *Factory { + return &Factory{} +} + +func (r *Factory) New(ctx context.Context, ghToken string) Client { + if r.Client != nil { + return NewClient(github.NewClient(r.Client)) + } + + tokenSrc := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ghToken}, + ) + tokenClient := oauth2.NewClient(ctx, tokenSrc) + return NewClient(github.NewClient(tokenClient)) +} diff --git a/pkg/registry/apis/provisioning/repository/github/impl.go b/pkg/registry/apis/provisioning/repository/github/impl.go new file mode 100644 index 00000000000..190080c3ccd --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/impl.go @@ -0,0 +1,770 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/google/go-github/v69/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type githubClient struct { + gh *github.Client +} + +var _ Client = (*githubClient)(nil) + +func NewClient(client *github.Client) *githubClient { + return &githubClient{client} +} + +func (r *githubClient) IsAuthenticated(ctx context.Context) error { + if _, _, err := r.gh.Users.Get(ctx, ""); err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + switch ghErr.Response.StatusCode { + case http.StatusUnauthorized: + return apierrors.NewUnauthorized("token is invalid or expired") + case http.StatusForbidden: + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusUnauthorized, + Reason: metav1.StatusReasonUnauthorized, + Message: "token is revoked or has insufficient permissions", + }, + } + case http.StatusServiceUnavailable: + return ErrServiceUnavailable + } + } + + return err + } + + return nil +} + +func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) { + _, resp, err := r.gh.Repositories.Get(ctx, owner, repository) + if err == nil { + return true, nil + } + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + + return false, err +} + +const ( + maxDirectoryItems = 1000 // Maximum number of items allowed in a directory + maxTreeItems = 10000 // Maximum number of items allowed in a tree + maxCommits = 1000 // Maximum number of commits to fetch + maxCompareFiles = 1000 // Maximum number of files to compare between commits + maxWebhooks = 100 // Maximum number of webhooks allowed per repository + maxPRFiles = 1000 // Maximum number of files allowed in a pull request + maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request + maxFileSize = 10 * 1024 * 1024 // 10MB in bytes +) + +func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) { + // First try to get repository contents + opts := &github.RepositoryContentGetOptions{ + Ref: ref, + } + + fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts) + if err != nil { + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return nil, nil, err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return nil, nil, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, nil, ErrResourceNotFound + } + return nil, nil, err + } + + if fc != nil { + // Check file size before returning content + if fc.GetSize() > maxFileSize { + return nil, nil, ErrFileTooLarge + } + return realRepositoryContent{fc}, nil, nil + } + + // For directories, check size limits + if len(dc) > maxDirectoryItems { + return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems) + } + + // Convert directory contents + allContents := make([]RepositoryContent, 0, len(dc)) + for _, original := range dc { + allContents = append(allContents, realRepositoryContent{original}) + } + + return nil, allContents, nil +} + +func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) { + var tree *github.Tree + var err error + + subPaths := safepath.Split(basePath) + currentRef := ref + + for { + // If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children. + recursive := recursive && len(subPaths) == 0 + + tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive) + if err != nil { + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return nil, false, err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return nil, false, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + if currentRef != ref { + // We're operating with a subpath which doesn't exist yet. + // Pretend as if there is simply no files. + return nil, false, nil + } + // currentRef == ref + // This indicates the repository or commitish reference doesn't exist. This should always return an error. + return nil, false, ErrResourceNotFound + } + return nil, false, err + } + + // Check if we've exceeded the maximum allowed items + if len(tree.Entries) > maxTreeItems { + return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems) + } + + // Prep for next iteration. + if len(subPaths) == 0 { + // We're done: we've discovered the tree we want. + break + } + + // the ref must be equal the SHA of the entry corresponding to subPaths[0] + currentRef = "" + for _, e := range tree.Entries { + if e.GetPath() == subPaths[0] { + currentRef = e.GetSHA() + break + } + } + subPaths = subPaths[1:] + if currentRef == "" { + // We couldn't find the folder in the tree... + return nil, false, nil + } + } + + // If the tree is truncated and we're in recursive mode, return an error + if tree.GetTruncated() && recursive { + return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems) + } + + entries := make([]RepositoryContent, 0, len(tree.Entries)) + for _, te := range tree.Entries { + rrc := &realRepositoryContent{ + real: &github.RepositoryContent{ + Path: te.Path, + Size: te.Size, + SHA: te.SHA, + }, + } + if te.GetType() == "tree" { + rrc.real.Type = github.Ptr("dir") + } else { + rrc.real.Type = te.Type + } + entries = append(entries, rrc) + } + return entries, tree.GetTruncated(), nil +} + +func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error { + if message == "" { + message = fmt.Sprintf("Create %s", path) + } + + _, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ + Branch: &branch, + Message: &message, + Content: content, + }) + if err == nil { + return nil + } + + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return err + } + if ghErr.Response.StatusCode == http.StatusUnprocessableEntity { + return ErrResourceAlreadyExists + } + return err +} + +func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error { + if message == "" { + message = fmt.Sprintf("Update %s", path) + } + + _, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ + Branch: &branch, + Message: &message, + Content: content, + SHA: &hash, + }) + if err == nil { + return nil + } + + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return err + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return ErrResourceNotFound + } + if ghErr.Response.StatusCode == http.StatusConflict { + return ErrMismatchedHash + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + return err +} + +func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error { + if message == "" { + message = fmt.Sprintf("Delete %s", path) + } + + _, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ + Branch: &branch, + Message: &message, + SHA: &hash, + }) + if err == nil { + return nil + } + + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return err + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return ErrResourceNotFound + } + if ghErr.Response.StatusCode == http.StatusConflict { + return ErrMismatchedHash + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + return err +} + +// Commits returns a list of commits for a given repository and branch. +func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) { + listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) { + return r.gh.Repositories.ListCommits(ctx, owner, repository, &github.CommitsListOptions{ + Path: path, + SHA: branch, + ListOptions: *opts, + }) + } + + commits, err := paginatedList( + ctx, + listFn, + defaultListOptions(maxCommits), + ) + if errors.Is(err, ErrTooManyItems) { + return nil, fmt.Errorf("too many commits to fetch (more than %d)", maxCommits) + } + if err != nil { + return nil, err + } + + ret := make([]Commit, 0, len(commits)) + for _, c := range commits { + var createdAt time.Time + var author *CommitAuthor + if c.GetCommit().GetAuthor() != nil { + author = &CommitAuthor{ + Name: c.GetCommit().GetAuthor().GetName(), + Username: c.GetAuthor().GetLogin(), + AvatarURL: c.GetAuthor().GetAvatarURL(), + } + + createdAt = c.GetCommit().GetAuthor().GetDate().Time + } + + var committer *CommitAuthor + if c.GetCommitter() != nil { + committer = &CommitAuthor{ + Name: c.GetCommit().GetCommitter().GetName(), + Username: c.GetCommitter().GetLogin(), + AvatarURL: c.GetCommitter().GetAvatarURL(), + } + } + + ret = append(ret, Commit{ + Ref: c.GetSHA(), + Message: c.GetCommit().GetMessage(), + Author: author, + Committer: committer, + CreatedAt: createdAt, + }) + } + + return ret, nil +} + +func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) { + listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { + compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts) + if err != nil { + return nil, resp, err + } + return compare.Files, resp, nil + } + + files, err := paginatedList( + ctx, + listFn, + defaultListOptions(maxCompareFiles), + ) + if errors.Is(err, ErrTooManyItems) { + return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles) + } + if err != nil { + return nil, err + } + + // Convert to the interface type + ret := make([]CommitFile, 0, len(files)) + for _, f := range files { + ret = append(ret, f) + } + + return ret, nil +} + +func (r *githubClient) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) { + branch, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) + if err != nil { + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return Branch{}, err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return Branch{}, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return Branch{}, ErrResourceNotFound + } + return Branch{}, err + } + + return Branch{ + Name: branch.GetName(), + Sha: branch.GetCommit().GetSHA(), + }, nil +} + +func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error { + // Fail if the branch already exists + if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil { + return ErrResourceAlreadyExists + } + + // Branch out based on the repository branch + baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0) + if err != nil { + return fmt.Errorf("get base branch: %w", err) + } + + if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{ + Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)), + Object: &github.GitObject{ + SHA: baseRef.Commit.SHA, + }, + }); err != nil { + return fmt.Errorf("create branch ref: %w", err) + } + + return nil +} + +func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) { + _, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) + if err == nil { + return true, nil + } + + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + + return false, err +} + +func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) { + listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { + return r.gh.Repositories.ListHooks(ctx, owner, repository, opts) + } + + hooks, err := paginatedList( + ctx, + listFn, + defaultListOptions(maxWebhooks), + ) + if errors.Is(err, ErrTooManyItems) { + return nil, fmt.Errorf("too many webhooks configured (more than %d)", maxWebhooks) + } + if err != nil { + return nil, err + } + + // Pre-allocate the result slice + ret := make([]WebhookConfig, 0, len(hooks)) + for _, h := range hooks { + contentType := h.GetConfig().GetContentType() + if contentType == "" { + contentType = "form" + } + + ret = append(ret, WebhookConfig{ + ID: h.GetID(), + Events: h.Events, + Active: h.GetActive(), + URL: h.GetConfig().GetURL(), + ContentType: contentType, + // Intentionally not setting Secret. + }) + } + return ret, nil +} + +func (r *githubClient) CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) { + if cfg.ContentType == "" { + cfg.ContentType = "form" + } + + hook := &github.Hook{ + URL: &cfg.URL, + Events: cfg.Events, + Active: &cfg.Active, + Config: &github.HookConfig{ + ContentType: &cfg.ContentType, + Secret: &cfg.Secret, + URL: &cfg.URL, + }, + } + + createdHook, _, err := r.gh.Repositories.CreateHook(ctx, owner, repository, hook) + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return WebhookConfig{}, ErrServiceUnavailable + } + if err != nil { + return WebhookConfig{}, err + } + + return WebhookConfig{ + ID: createdHook.GetID(), + // events is not returned by GitHub. + Events: cfg.Events, + Active: createdHook.GetActive(), + URL: createdHook.GetConfig().GetURL(), + ContentType: createdHook.GetConfig().GetContentType(), + // Secret is not returned by GitHub. + Secret: cfg.Secret, + }, nil +} + +func (r *githubClient) GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) { + hook, _, err := r.gh.Repositories.GetHook(ctx, owner, repository, webhookID) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return WebhookConfig{}, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return WebhookConfig{}, ErrResourceNotFound + } + return WebhookConfig{}, err + } + + contentType := hook.GetConfig().GetContentType() + if contentType == "" { + contentType = "json" + } + + return WebhookConfig{ + ID: hook.GetID(), + Events: hook.Events, + Active: hook.GetActive(), + URL: hook.GetConfig().GetURL(), + ContentType: contentType, + // Intentionally not setting Secret. + }, nil +} + +func (r *githubClient) DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error { + _, err := r.gh.Repositories.DeleteHook(ctx, owner, repository, webhookID) + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return ErrResourceNotFound + } + return err +} + +func (r *githubClient) EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error { + if cfg.ContentType == "" { + cfg.ContentType = "form" + } + + hook := &github.Hook{ + URL: &cfg.URL, + Events: cfg.Events, + Active: &cfg.Active, + Config: &github.HookConfig{ + ContentType: &cfg.ContentType, + Secret: &cfg.Secret, + URL: &cfg.URL, + }, + } + _, _, err := r.gh.Repositories.EditHook(ctx, owner, repository, cfg.ID, hook) + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + return err +} + +func (r *githubClient) ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) { + listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { + return r.gh.PullRequests.ListFiles(ctx, owner, repository, number, opts) + } + + files, err := paginatedList( + ctx, + listFn, + defaultListOptions(maxPRFiles), + ) + if errors.Is(err, ErrTooManyItems) { + return nil, fmt.Errorf("pull request contains too many files (more than %d)", maxPRFiles) + } + if err != nil { + return nil, err + } + + // Convert to the interface type + ret := make([]CommitFile, 0, len(files)) + for _, f := range files { + ret = append(ret, f) + } + + return ret, nil +} + +func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error { + comment := &github.IssueComment{ + Body: &body, + } + + if _, _, err := r.gh.Issues.CreateComment(ctx, owner, repository, number, comment); err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + return err + } + + return nil +} + +func (r *githubClient) CreatePullRequestFileComment(ctx context.Context, owner, repository string, number int, comment FileComment) error { + commentRequest := &github.PullRequestComment{ + Body: &comment.Content, + CommitID: &comment.Ref, + Path: &comment.Path, + Position: &comment.Position, + } + + if _, _, err := r.gh.PullRequests.CreateComment(ctx, owner, repository, number, commentRequest); err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return ErrServiceUnavailable + } + + return err + } + + return nil +} + +func (r *githubClient) ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error { + listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.PullRequestComment, *github.Response, error) { + return r.gh.PullRequests.ListComments(ctx, owner, repository, number, &github.PullRequestListCommentsOptions{ + ListOptions: *opts, + }) + } + + comments, err := paginatedList(ctx, listFn, defaultListOptions(maxPullRequestsFileComments)) + if errors.Is(err, ErrTooManyItems) { + return fmt.Errorf("too many comments to process (more than %d)", maxPullRequestsFileComments) + } + if err != nil { + return err + } + + userLogin, _, err := r.gh.Users.Get(ctx, "") + if err != nil { + return fmt.Errorf("get user: %w", err) + } + + for _, c := range comments { + // skip if comments were not created by us + if c.User.GetLogin() != userLogin.GetLogin() { + continue + } + + if _, err := r.gh.PullRequests.DeleteComment(ctx, owner, repository, c.GetID()); err != nil { + return fmt.Errorf("delete comment: %w", err) + } + } + + return nil +} + +type realRepositoryContent struct { + real *github.RepositoryContent +} + +var _ RepositoryContent = realRepositoryContent{} + +func (c realRepositoryContent) IsDirectory() bool { + return c.real.GetType() == "dir" +} + +func (c realRepositoryContent) GetFileContent() (string, error) { + return c.real.GetContent() +} + +func (c realRepositoryContent) IsSymlink() bool { + return c.real.Target != nil +} + +func (c realRepositoryContent) GetPath() string { + return c.real.GetPath() +} + +func (c realRepositoryContent) GetSHA() string { + return c.real.GetSHA() +} + +func (c realRepositoryContent) GetSize() int64 { + if c.real.Size != nil { + return int64(*c.real.Size) + } + if c.real.Content != nil { + if c, err := c.real.GetContent(); err == nil { + return int64(len(c)) + } + } + return 0 +} + +// listOptions represents pagination parameters for list operations +type listOptions struct { + github.ListOptions + MaxItems int +} + +// defaultListOptions returns a ListOptions with sensible defaults +func defaultListOptions(maxItems int) listOptions { + return listOptions{ + ListOptions: github.ListOptions{ + Page: 1, + PerPage: 100, + }, + MaxItems: maxItems, + } +} + +// paginatedList is a generic function to handle GitHub API pagination +func paginatedList[T any]( + ctx context.Context, + listFn func(context.Context, *github.ListOptions) ([]T, *github.Response, error), + opts listOptions, +) ([]T, error) { + var allItems []T + + for { + items, resp, err := listFn(ctx, &opts.ListOptions) + if err != nil { + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return nil, err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return nil, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, ErrResourceNotFound + } + return nil, err + } + + // Pre-allocate the slice if this is the first page + if allItems == nil { + allItems = make([]T, 0, len(items)*2) // Estimate double the first page size + } + + allItems = append(allItems, items...) + + // Check if we've exceeded the maximum allowed items + if len(allItems) > opts.MaxItems { + return nil, ErrTooManyItems + } + + // If there are no more pages, break + if resp.NextPage == 0 { + break + } + + // Set up next page + opts.Page = resp.NextPage + } + + return allItems, nil +} diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-issue_comment-created.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-issue_comment-created.json new file mode 100644 index 00000000000..88fef6e3b97 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-issue_comment-created.json @@ -0,0 +1,231 @@ +{ + "action": "created", + "issue": { + "id": 2726065547, + "number": 12, + "state": "open", + "locked": false, + "title": "Webhook test PR", + "author_association": "MEMBER", + "user": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "comments": 1, + "created_at": "2024-12-09T05:53:14Z", + "updated_at": "2024-12-09T06:03:21Z", + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12", + "html_url": "https://github.com/grafana/git-ui-sync-demo/pull/12", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/comments", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/events", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/labels{/name}", + "repository_url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "pull_request": { + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12", + "html_url": "https://github.com/grafana/git-ui-sync-demo/pull/12", + "diff_url": "https://github.com/grafana/git-ui-sync-demo/pull/12.diff", + "patch_url": "https://github.com/grafana/git-ui-sync-demo/pull/12.patch" + }, + "reactions": { + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "confused": 0, + "heart": 0, + "hooray": 0, + "rocket": 0, + "eyes": 0, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/reactions" + }, + "node_id": "PR_kwDONO4cS86EfDVR", + "draft": false + }, + "comment": { + "id": 2527008082, + "node_id": "IC_kwDONO4cS86WnxVS", + "body": "comment in PR", + "user": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "reactions": { + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "confused": 0, + "heart": 0, + "hooray": 0, + "rocket": 0, + "eyes": 0, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments/2527008082/reactions" + }, + "created_at": "2024-12-09T06:03:19Z", + "updated_at": "2024-12-09T06:03:19Z", + "author_association": "MEMBER", + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments/2527008082", + "html_url": "https://github.com/grafana/git-ui-sync-demo/pull/12#issuecomment-2527008082", + "issue_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12" + }, + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "default_branch": "main", + "created_at": "2024-11-13T17:13:33Z", + "pushed_at": "2024-12-09T05:58:00Z", + "updated_at": "2024-12-09T05:58:03Z", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "fork": false, + "forks_count": 0, + "open_issues_count": 9, + "open_issues": 9, + "stargazers_count": 0, + "watchers_count": 0, + "watchers": 0, + "size": 141, + "allow_forking": false, + "web_commit_signoff_required": false, + "archived": false, + "disabled": false, + "private": true, + "has_issues": true, + "has_wiki": true, + "has_pages": false, + "has_projects": true, + "has_downloads": true, + "has_discussions": false, + "is_template": false, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "visibility": "internal" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "organization": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open \u0026 composable observability platform.", + "url": "https://api.github.com/orgs/grafana", + "events_url": "https://api.github.com/orgs/grafana/events", + "hooks_url": "https://api.github.com/orgs/grafana/hooks", + "issues_url": "https://api.github.com/orgs/grafana/issues", + "members_url": "https://api.github.com/orgs/grafana/members{/member}", + "public_members_url": "https://api.github.com/orgs/grafana/public_members{/member}", + "repos_url": "https://api.github.com/orgs/grafana/repos" + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-ping-check.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-ping-check.json new file mode 100644 index 00000000000..65db1848748 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-ping-check.json @@ -0,0 +1,143 @@ +{ + "zen": "Keep it logically awesome.", + "hook_id": 517704995, + "hook": { + "created_at": "2024-12-09T05:44:20Z", + "updated_at": "2024-12-09T05:44:20Z", + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks/517704995", + "id": 517704995, + "type": "Repository", + "name": "web", + "test_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks/517704995/test", + "ping_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks/517704995/pings", + "last_response": { + "code": null, + "message": null, + "status": "unused" + }, + "config": { + "content_type": "form", + "insecure_ssl": "0", + "url": "https://0b71-216-128-0-90.ngrok-free.app/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/github-example-ryan/webhook", + "secret": "********" + }, + "events": [ + "*" + ], + "active": true + }, + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "default_branch": "main", + "created_at": "2024-11-13T17:13:33Z", + "pushed_at": "2024-12-08T10:51:24Z", + "updated_at": "2024-11-28T12:53:26Z", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "fork": false, + "forks_count": 0, + "open_issues_count": 8, + "open_issues": 8, + "stargazers_count": 0, + "watchers_count": 0, + "watchers": 0, + "size": 141, + "allow_forking": false, + "web_commit_signoff_required": false, + "archived": false, + "disabled": false, + "private": true, + "has_issues": true, + "has_wiki": true, + "has_pages": false, + "has_projects": true, + "has_downloads": true, + "has_discussions": false, + "is_template": false, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "visibility": "internal" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-pull_request-opened.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-pull_request-opened.json new file mode 100644 index 00000000000..4963dce6eee --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-pull_request-opened.json @@ -0,0 +1,470 @@ + +{ + "action": "opened", + "number": 12, + "pull_request": { + "id": 2222732625, + "number": 12, + "state": "open", + "locked": false, + "title": "Webhook test PR", + "created_at": "2024-12-09T05:53:14Z", + "updated_at": "2024-12-09T05:53:14Z", + "user": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "draft": false, + "merged": false, + "mergeable_state": "unknown", + "comments": 0, + "commits": 1, + "additions": 1, + "deletions": 0, + "changed_files": 1, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12", + "html_url": "https://github.com/grafana/git-ui-sync-demo/pull/12", + "issue_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/ab5446a53df9e5f8bdeed52250f51fad08e822bc", + "diff_url": "https://github.com/grafana/git-ui-sync-demo/pull/12.diff", + "patch_url": "https://github.com/grafana/git-ui-sync-demo/pull/12.patch", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12/commits", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/comments", + "review_comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12/comments", + "review_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/comments{/number}", + "review_comments": 0, + "maintainer_can_modify": false, + "author_association": "MEMBER", + "node_id": "PR_kwDONO4cS86EfDVR", + "_links": { + "self": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12" + }, + "html": { + "href": "https://github.com/grafana/git-ui-sync-demo/pull/12" + }, + "issue": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12" + }, + "comments": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/12/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls/12/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/ab5446a53df9e5f8bdeed52250f51fad08e822bc" + } + }, + "head": { + "label": "grafana:dashboard/1733653266690", + "ref": "dashboard/1733653266690", + "sha": "ab5446a53df9e5f8bdeed52250f51fad08e822bc", + "repo": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "default_branch": "main", + "created_at": "2024-11-13T17:13:33Z", + "pushed_at": "2024-12-08T10:51:24Z", + "updated_at": "2024-11-28T12:53:26Z", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "fork": false, + "forks_count": 0, + "open_issues_count": 9, + "open_issues": 9, + "stargazers_count": 0, + "watchers_count": 0, + "watchers": 0, + "size": 141, + "allow_rebase_merge": true, + "allow_update_branch": false, + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_auto_merge": false, + "allow_forking": false, + "web_commit_signoff_required": false, + "delete_branch_on_merge": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "merge_commit_title": "MERGE_MESSAGE", + "merge_commit_message": "PR_TITLE", + "archived": false, + "disabled": false, + "private": true, + "has_issues": true, + "has_wiki": true, + "has_pages": false, + "has_projects": true, + "has_downloads": true, + "has_discussions": false, + "is_template": false, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "visibility": "internal" + }, + "user": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + } + }, + "base": { + "label": "grafana:main", + "ref": "main", + "sha": "6c86a0cdfd220c2fe3518cfaa4a4babf030d9a7a", + "repo": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "default_branch": "main", + "created_at": "2024-11-13T17:13:33Z", + "pushed_at": "2024-12-08T10:51:24Z", + "updated_at": "2024-11-28T12:53:26Z", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "fork": false, + "forks_count": 0, + "open_issues_count": 9, + "open_issues": 9, + "stargazers_count": 0, + "watchers_count": 0, + "watchers": 0, + "size": 141, + "allow_rebase_merge": true, + "allow_update_branch": false, + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_auto_merge": false, + "allow_forking": false, + "web_commit_signoff_required": false, + "delete_branch_on_merge": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "merge_commit_title": "MERGE_MESSAGE", + "merge_commit_message": "PR_TITLE", + "archived": false, + "disabled": false, + "private": true, + "has_issues": true, + "has_wiki": true, + "has_pages": false, + "has_projects": true, + "has_downloads": true, + "has_discussions": false, + "is_template": false, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "visibility": "internal" + }, + "user": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + } + } + }, + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "default_branch": "main", + "created_at": "2024-11-13T17:13:33Z", + "pushed_at": "2024-12-08T10:51:24Z", + "updated_at": "2024-11-28T12:53:26Z", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "fork": false, + "forks_count": 0, + "open_issues_count": 9, + "open_issues": 9, + "stargazers_count": 0, + "watchers_count": 0, + "watchers": 0, + "size": 141, + "allow_forking": false, + "web_commit_signoff_required": false, + "archived": false, + "disabled": false, + "private": true, + "has_issues": true, + "has_wiki": true, + "has_pages": false, + "has_projects": true, + "has_downloads": true, + "has_discussions": false, + "is_template": false, + "url": "https://api.github.com/repos/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "visibility": "internal" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "organization": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open \u0026 composable observability platform.", + "url": "https://api.github.com/orgs/grafana", + "events_url": "https://api.github.com/orgs/grafana/events", + "hooks_url": "https://api.github.com/orgs/grafana/hooks", + "issues_url": "https://api.github.com/orgs/grafana/issues", + "members_url": "https://api.github.com/orgs/grafana/members{/member}", + "public_members_url": "https://api.github.com/orgs/grafana/public_members{/member}", + "repos_url": "https://api.github.com/orgs/grafana/repos" + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-different_branch.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-different_branch.json new file mode 100644 index 00000000000..756ebd5d9f1 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-different_branch.json @@ -0,0 +1,148 @@ +{ + "ref": "refs/heads/not-main", + "commits": [ + { + "message": "Update README.md\n\ntest message", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "url": "https://github.com/grafana/git-ui-sync-demo/commit/72096e3adc646c5a5b8a91744f962b12bac06045", + "distinct": true, + "id": "72096e3adc646c5a5b8a91744f962b12bac06045", + "tree_id": "03ff034c54bcefae2f96041f3fb8172f2fe93df3", + "timestamp": "2024-12-09T08:58:00+03:00", + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "modified": [ + "README.md" + ] + } + ], + "before": "6c86a0cdfd220c2fe3518cfaa4a4babf030d9a7a", + "after": "72096e3adc646c5a5b8a91744f962b12bac06045", + "created": false, + "deleted": false, + "forced": false, + "compare": "https://github.com/grafana/git-ui-sync-demo/compare/6c86a0cdfd22...72096e3adc64", + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "name": "grafana", + "email": "hello@grafana.com", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "private": true, + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "fork": false, + "created_at": "2024-11-13T20:13:33+03:00", + "pushed_at": "2024-12-09T08:58:00+03:00", + "updated_at": "2024-11-28T12:53:26Z", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "size": 141, + "stargazers_count": 0, + "watchers_count": 0, + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "archived": false, + "disabled": false, + "open_issues_count": 9, + "default_branch": "main", + "master_branch": "main", + "organization": "grafana", + "url": "https://github.com/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo" + }, + "head_commit": { + "message": "Update README.md\n\ntest message", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "url": "https://github.com/grafana/git-ui-sync-demo/commit/72096e3adc646c5a5b8a91744f962b12bac06045", + "distinct": true, + "id": "72096e3adc646c5a5b8a91744f962b12bac06045", + "tree_id": "03ff034c54bcefae2f96041f3fb8172f2fe93df3", + "timestamp": "2024-12-09T08:58:00+03:00", + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "modified": [ + "README.md" + ] + }, + "pusher": { + "name": "ryantxu", + "email": "ryantxu@gmail.com" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "organization": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open \u0026 composable observability platform.", + "url": "https://api.github.com/orgs/grafana", + "events_url": "https://api.github.com/orgs/grafana/events", + "hooks_url": "https://api.github.com/orgs/grafana/hooks", + "issues_url": "https://api.github.com/orgs/grafana/issues", + "members_url": "https://api.github.com/orgs/grafana/members{/member}", + "public_members_url": "https://api.github.com/orgs/grafana/public_members{/member}", + "repos_url": "https://api.github.com/orgs/grafana/repos" + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nested.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nested.json new file mode 100644 index 00000000000..6e641869c7e --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nested.json @@ -0,0 +1,234 @@ +{ + "ref": "refs/heads/main", + "before": "72096e3adc646c5a5b8a91744f962b12bac06045", + "after": "5c816f9812e391c62b0c5555d0b473b296d9179c", + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "private": true, + "owner": { + "name": "grafana", + "email": "hello@grafana.com", + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/grafana", + "html_url": "https://github.com/grafana", + "followers_url": "https://api.github.com/users/grafana/followers", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "repos_url": "https://api.github.com/users/grafana/repos", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "fork": false, + "url": "https://github.com/grafana/git-ui-sync-demo", + "forks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/forks", + "keys_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/teams", + "hooks_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/hooks", + "issue_events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/events{/number}", + "events_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/events", + "assignees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/assignees{/user}", + "branches_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/branches{/branch}", + "tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/tags", + "blobs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/languages", + "stargazers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/stargazers", + "contributors_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contributors", + "subscribers_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscribers", + "subscription_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/subscription", + "commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/contents/{+path}", + "compare_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/merges", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/downloads", + "issues_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/issues{/number}", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/labels{/name}", + "releases_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/releases{/id}", + "deployments_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/deployments", + "created_at": 1731518013, + "updated_at": "2024-12-09T05:58:03Z", + "pushed_at": 1733731254, + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo", + "homepage": null, + "size": 142, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 9, + "license": null, + "allow_forking": false, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + + ], + "visibility": "internal", + "forks": 0, + "open_issues": 9, + "watchers": 0, + "default_branch": "main", + "stargazers": 0, + "master_branch": "main", + "organization": "grafana", + "custom_properties": { + + } + }, + "pusher": { + "name": "ryantxu", + "email": "ryantxu@gmail.com" + }, + "organization": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "url": "https://api.github.com/orgs/grafana", + "repos_url": "https://api.github.com/orgs/grafana/repos", + "events_url": "https://api.github.com/orgs/grafana/events", + "hooks_url": "https://api.github.com/orgs/grafana/hooks", + "issues_url": "https://api.github.com/orgs/grafana/issues", + "members_url": "https://api.github.com/orgs/grafana/members{/member}", + "public_members_url": "https://api.github.com/orgs/grafana/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open & composable observability platform." + }, + "enterprise": { + "id": 129980, + "slug": "grafana", + "name": "Grafana Labs Enterprise", + "node_id": "E_kgDOAAH7vA", + "avatar_url": "https://avatars.githubusercontent.com/b/129980?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open & composable observability platform.", + "website_url": "https://grafana.com", + "html_url": "https://github.com/enterprises/grafana", + "created_at": "2024-02-29T23:01:47Z", + "updated_at": "2024-10-21T08:45:28Z" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ryantxu", + "html_url": "https://github.com/ryantxu", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "created": false, + "deleted": false, + "forced": false, + "base_ref": null, + "compare": "https://github.com/grafana/git-ui-sync-demo/compare/72096e3adc64...5c816f9812e3", + "commits": [ + { + "id": "5c816f9812e391c62b0c5555d0b473b296d9179c", + "tree_id": "97c7ba756b07b6a2b7fd130e59f75deed53fe027", + "distinct": true, + "message": "nested folders", + "timestamp": "2024-12-09T11:00:48+03:00", + "url": "https://github.com/grafana/git-ui-sync-demo/commit/5c816f9812e391c62b0c5555d0b473b296d9179c", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "committer": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "added": [ + "nested-1/README.md", + "nested-1/dash-1.json", + "nested-1/nested-2/README.md", + "nested-1/nested-2/dash-2.json" + ], + "removed": [ + + ], + "modified": [ + "first-dashboard.json" + ] + } + ], + "head_commit": { + "id": "5c816f9812e391c62b0c5555d0b473b296d9179c", + "tree_id": "97c7ba756b07b6a2b7fd130e59f75deed53fe027", + "distinct": true, + "message": "nested folders", + "timestamp": "2024-12-09T11:00:48+03:00", + "url": "https://github.com/grafana/git-ui-sync-demo/commit/5c816f9812e391c62b0c5555d0b473b296d9179c", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "committer": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "added": [ + "nested-1/README.md", + "nested-1/dash-1.json", + "nested-1/nested-2/README.md", + "nested-1/nested-2/dash-2.json" + ], + "removed": [ + + ], + "modified": [ + "first-dashboard.json" + ] + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nothing_relevant.json b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nothing_relevant.json new file mode 100644 index 00000000000..b228d10a9c6 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/testdata/webhook-push-nothing_relevant.json @@ -0,0 +1,148 @@ +{ + "ref": "refs/heads/main", + "commits": [ + { + "message": "Update README.md\n\ntest message", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "url": "https://github.com/grafana/git-ui-sync-demo/commit/72096e3adc646c5a5b8a91744f962b12bac06045", + "distinct": true, + "id": "72096e3adc646c5a5b8a91744f962b12bac06045", + "tree_id": "03ff034c54bcefae2f96041f3fb8172f2fe93df3", + "timestamp": "2024-12-09T08:58:00+03:00", + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "modified": [ + "README.md" + ] + } + ], + "before": "6c86a0cdfd220c2fe3518cfaa4a4babf030d9a7a", + "after": "72096e3adc646c5a5b8a91744f962b12bac06045", + "created": false, + "deleted": false, + "forced": false, + "compare": "https://github.com/grafana/git-ui-sync-demo/compare/6c86a0cdfd22...72096e3adc64", + "repository": { + "id": 888020043, + "node_id": "R_kgDONO4cSw", + "name": "git-ui-sync-demo", + "full_name": "grafana/git-ui-sync-demo", + "owner": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "html_url": "https://github.com/grafana", + "gravatar_id": "", + "name": "grafana", + "email": "hello@grafana.com", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/grafana", + "events_url": "https://api.github.com/users/grafana/events{/privacy}", + "following_url": "https://api.github.com/users/grafana/following{/other_user}", + "followers_url": "https://api.github.com/users/grafana/followers", + "gists_url": "https://api.github.com/users/grafana/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/grafana/orgs", + "received_events_url": "https://api.github.com/users/grafana/received_events", + "repos_url": "https://api.github.com/users/grafana/repos", + "starred_url": "https://api.github.com/users/grafana/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/grafana/subscriptions" + }, + "private": true, + "description": "A repository containing Grafana dashboards to demo the Github Sync feature in Grafana.", + "fork": false, + "created_at": "2024-11-13T20:13:33+03:00", + "pushed_at": "2024-12-09T08:58:00+03:00", + "updated_at": "2024-11-28T12:53:26Z", + "pulls_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/pulls{/number}", + "size": 141, + "stargazers_count": 0, + "watchers_count": 0, + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "archived": false, + "disabled": false, + "open_issues_count": 9, + "default_branch": "main", + "master_branch": "main", + "organization": "grafana", + "url": "https://github.com/grafana/git-ui-sync-demo", + "archive_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/{archive_format}{/ref}", + "html_url": "https://github.com/grafana/git-ui-sync-demo", + "statuses_url": "https://api.github.com/repos/grafana/git-ui-sync-demo/statuses/{sha}", + "git_url": "git://github.com/grafana/git-ui-sync-demo.git", + "ssh_url": "git@github.com:grafana/git-ui-sync-demo.git", + "clone_url": "https://github.com/grafana/git-ui-sync-demo.git", + "svn_url": "https://github.com/grafana/git-ui-sync-demo" + }, + "head_commit": { + "message": "Update README.md\n\ntest message", + "author": { + "name": "Ryan McKinley", + "email": "ryantxu@gmail.com", + "username": "ryantxu" + }, + "url": "https://github.com/grafana/git-ui-sync-demo/commit/72096e3adc646c5a5b8a91744f962b12bac06045", + "distinct": true, + "id": "72096e3adc646c5a5b8a91744f962b12bac06045", + "tree_id": "03ff034c54bcefae2f96041f3fb8172f2fe93df3", + "timestamp": "2024-12-09T08:58:00+03:00", + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "modified": [ + "README.md" + ] + }, + "pusher": { + "name": "ryantxu", + "email": "ryantxu@gmail.com" + }, + "sender": { + "login": "ryantxu", + "id": 705951, + "node_id": "MDQ6VXNlcjcwNTk1MQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/705951?v=4", + "html_url": "https://github.com/ryantxu", + "gravatar_id": "", + "type": "User", + "site_admin": false, + "url": "https://api.github.com/users/ryantxu", + "events_url": "https://api.github.com/users/ryantxu/events{/privacy}", + "following_url": "https://api.github.com/users/ryantxu/following{/other_user}", + "followers_url": "https://api.github.com/users/ryantxu/followers", + "gists_url": "https://api.github.com/users/ryantxu/gists{/gist_id}", + "organizations_url": "https://api.github.com/users/ryantxu/orgs", + "received_events_url": "https://api.github.com/users/ryantxu/received_events", + "repos_url": "https://api.github.com/users/ryantxu/repos", + "starred_url": "https://api.github.com/users/ryantxu/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ryantxu/subscriptions" + }, + "organization": { + "login": "grafana", + "id": 7195757, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxOTU3NTc=", + "avatar_url": "https://avatars.githubusercontent.com/u/7195757?v=4", + "description": "Grafana Labs is behind leading open source projects Grafana and Loki, and the creator of the first open \u0026 composable observability platform.", + "url": "https://api.github.com/orgs/grafana", + "events_url": "https://api.github.com/orgs/grafana/events", + "hooks_url": "https://api.github.com/orgs/grafana/hooks", + "issues_url": "https://api.github.com/orgs/grafana/issues", + "members_url": "https://api.github.com/orgs/grafana/members{/member}", + "public_members_url": "https://api.github.com/orgs/grafana/public_members{/member}", + "repos_url": "https://api.github.com/orgs/grafana/repos" + } +} \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/repository/github_test.go b/pkg/registry/apis/provisioning/repository/github_test.go new file mode 100644 index 00000000000..dc24f6484e4 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github_test.go @@ -0,0 +1,149 @@ +package repository + +import ( + "fmt" + "net/http" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +func TestIsValidGitBranchName(t *testing.T) { + tests := []struct { + name string + branch string + expected bool + }{ + {"Valid branch name", "feature/add-tests", true}, + {"Valid branch name with numbers", "feature/123-add-tests", true}, + {"Valid branch name with dots", "feature.add.tests", true}, + {"Valid branch name with hyphens", "feature-add-tests", true}, + {"Valid branch name with underscores", "feature_add_tests", true}, + {"Valid branch name with mixed characters", "feature/add_tests-123", true}, + {"Starts with /", "/feature", false}, + {"Ends with /", "feature/", false}, + {"Ends with .", "feature.", false}, + {"Ends with space", "feature ", false}, + {"Contains consecutive slashes", "feature//branch", false}, + {"Contains consecutive dots", "feature..branch", false}, + {"Contains @{", "feature@{branch", false}, + {"Contains invalid character ~", "feature~branch", false}, + {"Contains invalid character ^", "feature^branch", false}, + {"Contains invalid character :", "feature:branch", false}, + {"Contains invalid character ?", "feature?branch", false}, + {"Contains invalid character *", "feature*branch", false}, + {"Contains invalid character [", "feature[branch", false}, + {"Contains invalid character ]", "feature]branch", false}, + {"Contains invalid character \\", "feature\\branch", false}, + {"Empty branch name", "", false}, + {"Only whitespace", " ", false}, + {"Single valid character", "a", true}, + {"Ends with .lock", "feature.lock", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isValidGitBranchName(tt.branch)) + }) + } +} + +func TestParseWebhooks(t *testing.T) { + tests := []struct { + messageType string + name string + expected provisioning.WebhookResponse + }{ + {"ping", "check", provisioning.WebhookResponse{ + Code: http.StatusOK, + }}, + {"pull_request", "opened", provisioning.WebhookResponse{ + Code: http.StatusAccepted, // 202 + Job: &provisioning.JobSpec{ + Repository: "unit-test-repo", + Action: provisioning.JobActionPullRequest, + PullRequest: &provisioning.PullRequestJobOptions{ + Ref: "dashboard/1733653266690", + Hash: "ab5446a53df9e5f8bdeed52250f51fad08e822bc", + PR: 12, + URL: "https://github.com/grafana/git-ui-sync-demo/pull/12", + }, + }, + }}, + {"push", "different_branch", provisioning.WebhookResponse{ + Code: http.StatusOK, // we don't care about a branch that isn't the one we configured + }}, + {"push", "nothing_relevant", provisioning.WebhookResponse{ + Code: http.StatusAccepted, + Job: &provisioning.JobSpec{ // we want to always push a sync job + Repository: "unit-test-repo", + Action: provisioning.JobActionSync, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }}, + {"push", "nested", provisioning.WebhookResponse{ + Code: http.StatusAccepted, + Job: &provisioning.JobSpec{ + Repository: "unit-test-repo", + Action: provisioning.JobActionSync, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }}, + {"issue_comment", "created", provisioning.WebhookResponse{ + Code: http.StatusNotImplemented, + }}, + } + + gh := &githubRepository{ + config: &provisioning.Repository{ + ObjectMeta: v1.ObjectMeta{ + Name: "unit-test-repo", + }, + Spec: provisioning.RepositorySpec{ + Sync: provisioning.SyncOptions{ + Enabled: true, // required to accept sync job + }, + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/git-ui-sync-demo", + Branch: "main", + + GenerateDashboardPreviews: true, + }, + }, + }, + } + var err error + gh.owner, gh.repo, err = parseOwnerRepo(gh.config.Spec.GitHub.URL) + require.NoError(t, err) + + // Support parsing from a ".git" extension + owner, repo, err := parseOwnerRepo(gh.config.Spec.GitHub.URL + ".git") + require.NoError(t, err) + require.Equal(t, gh.owner, owner) + require.Equal(t, gh.repo, repo) + + for _, tt := range tests { + name := fmt.Sprintf("webhook-%s-%s.json", tt.messageType, tt.name) + t.Run(name, func(t *testing.T) { + // nolint:gosec + payload, err := os.ReadFile(path.Join("github", "testdata", name)) + require.NoError(t, err) + + rsp, err := gh.parseWebhook(tt.messageType, payload) + require.NoError(t, err) + + require.Equal(t, tt.expected.Code, rsp.Code) + require.Equal(t, tt.expected.Job, rsp.Job) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport.go b/pkg/registry/apis/provisioning/repository/go-git/transport.go new file mode 100644 index 00000000000..652df25618a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/go-git/transport.go @@ -0,0 +1,71 @@ +package gogit + +import ( + "fmt" + "io" + "net/http" + "sync/atomic" +) + +var errBytesLimitExceeded = fmt.Errorf("bytes limit exceeded") + +// ByteLimitedTransport wraps http.RoundTripper to enforce a max byte limit +type ByteLimitedTransport struct { + Transport http.RoundTripper + Limit int64 + Bytes int64 +} + +// NewByteLimitedTransport creates a new ByteLimitedTransport with the specified transport and byte limit. +// If transport is nil, http.DefaultTransport will be used. +func NewByteLimitedTransport(transport http.RoundTripper, limit int64) *ByteLimitedTransport { + if transport == nil { + transport = http.DefaultTransport + } + return &ByteLimitedTransport{ + Transport: transport, + Limit: limit, + Bytes: 0, + } +} + +// RoundTrip tracks downloaded bytes and aborts if limit is exceeded +func (b *ByteLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := b.Transport.RoundTrip(req) + if err != nil { + return nil, err + } + + // Wrap response body to track bytes read + resp.Body = &byteLimitedReader{ + reader: resp.Body, + limit: b.Limit, + bytes: &b.Bytes, + } + + return resp, nil +} + +// byteLimitedReader tracks and enforces a download limit +type byteLimitedReader struct { + reader io.ReadCloser + limit int64 + bytes *int64 +} + +func (r *byteLimitedReader) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + if err != nil { + return n, err + } + + if atomic.AddInt64(r.bytes, int64(n)) > r.limit { + return 0, errBytesLimitExceeded + } + + return n, nil +} + +func (r *byteLimitedReader) Close() error { + return r.reader.Close() +} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go new file mode 100644 index 00000000000..77a59dff035 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go @@ -0,0 +1,139 @@ +package gogit + +import ( + "bytes" + "errors" + "io" + "net/http" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockTransport struct { + response *http.Response + err error +} + +func (m *mockTransport) RoundTrip(*http.Request) (*http.Response, error) { + return m.response, m.err +} + +func TestNewByteLimitedTransport(t *testing.T) { + tests := []struct { + name string + transport http.RoundTripper + limit int64 + }{ + { + name: "with custom transport", + transport: &mockTransport{}, + limit: 1000, + }, + { + name: "with nil transport", + transport: nil, + limit: 1000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + blt := NewByteLimitedTransport(tt.transport, tt.limit) + assert.NotNil(t, blt) + assert.Equal(t, tt.limit, blt.Limit) + assert.Equal(t, int64(0), blt.Bytes) + + if tt.transport == nil { + assert.Equal(t, http.DefaultTransport, blt.Transport) + } else { + assert.Equal(t, tt.transport, blt.Transport) + } + }) + } +} + +func TestByteLimitedTransport_RoundTrip(t *testing.T) { + tests := []struct { + name string + responseBody string + limit int64 + expectedError error + }{ + { + name: "under limit", + responseBody: "small response", + limit: 100, + expectedError: nil, + }, + { + name: "exceeds limit", + responseBody: "this response will exceed the byte limit", + limit: 10, + expectedError: errBytesLimitExceeded, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockResp := &http.Response{ + Body: io.NopCloser(bytes.NewBufferString(tt.responseBody)), + } + mockTransport := &mockTransport{response: mockResp} + + blt := NewByteLimitedTransport(mockTransport, tt.limit) + resp, err := blt.RoundTrip(&http.Request{}) + require.NoError(t, err) + defer func() { + closeErr := resp.Body.Close() + assert.NoError(t, closeErr, "failed to close response body") + }() + + data, err := io.ReadAll(resp.Body) + if tt.expectedError != nil { + assert.True(t, errors.Is(err, tt.expectedError), "expected error %v, got %v", tt.expectedError, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.responseBody, string(data)) + } + }) + } +} + +func TestByteLimitedReader_Close(t *testing.T) { + mockBody := io.NopCloser(bytes.NewBufferString("test")) + var byteCount int64 + reader := &byteLimitedReader{ + reader: mockBody, + limit: 100, + bytes: &byteCount, + } + + err := reader.Close() + assert.NoError(t, err) +} + +func TestByteLimitedReader_AtomicCounting(t *testing.T) { + var byteCount int64 + reader := &byteLimitedReader{ + reader: io.NopCloser(bytes.NewBufferString("test data")), + limit: 5, + bytes: &byteCount, + } + + // First read should succeed + buf := make([]byte, 4) + n, err := reader.Read(buf) + assert.NoError(t, err) + assert.Equal(t, 4, n) + + // Second read should fail due to limit + n, err = reader.Read(buf) + assert.True(t, errors.Is(err, errBytesLimitExceeded), "expected error %v, got %v", errBytesLimitExceeded, err) + assert.Equal(t, 0, n) + + // Verify atomic counter + assert.Greater(t, atomic.LoadInt64(&byteCount), int64(5)) +} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go new file mode 100644 index 00000000000..4447aa2174a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go @@ -0,0 +1,423 @@ +package gogit + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "strings" + "time" + + "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/client" + githttp "github.com/go-git/go-git/v5/plumbing/transport/http" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +const ( + // maxOperationBytes is the maximum size of a git operation in bytes (1 GB) + maxOperationBytes = int64(1 << 30) + maxOperationTimeout = 10 * time.Minute +) + +func init() { + // Create a size-limited writer that will cancel the context if size is exceeded + limitedTransport := NewByteLimitedTransport(http.DefaultTransport, maxOperationBytes) + httpClient := githttp.NewClient(&http.Client{ + Transport: limitedTransport, + }) + client.InstallProtocol("https", httpClient) + client.InstallProtocol("http", httpClient) +} + +var _ repository.Repository = (*GoGitRepo)(nil) + +type GoGitCloneOptions struct { + Root string // tempdir (when empty, memory??) + + // If the branch does not exist, create it + CreateIfNotExists bool + + // Skip intermediate commits and commit all before push + SingleCommitBeforePush bool + + // Maximum allowed size for repository clone in bytes (0 means no limit) + MaxSize int64 + + // Maximum time allowed for clone operation in seconds (0 means no limit) + Timeout time.Duration +} + +type GoGitPushOptions struct { + Timeout time.Duration +} + +type GoGitRepo struct { + config *provisioning.Repository + opts GoGitCloneOptions + decryptedPassword string + + repo *git.Repository + tree *git.Worktree + dir string // file path to worktree root (necessary? should use billy) +} + +// This will create a new clone every time +// As structured, it is valid for one context and should not be shared across multiple requests +func Clone( + ctx context.Context, + config *provisioning.Repository, + opts GoGitCloneOptions, + secrets secrets.Service, + progress io.Writer, // os.Stdout +) (*GoGitRepo, error) { + if opts.Root == "" { + return nil, fmt.Errorf("missing root config") + } + + // add a timeout to the operation + timeout := maxOperationTimeout + if opts.Timeout > 0 { + timeout = opts.Timeout + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) + if err != nil { + return nil, fmt.Errorf("error decrypting token: %w", err) + } + + if err := os.MkdirAll(opts.Root, 0700); err != nil { + return nil, fmt.Errorf("create root dir: %w", err) + } + + dir, err := mkdirTempClone(opts.Root, config) + if err != nil { + return nil, fmt.Errorf("create temp clone dir: %w", err) + } + + repo, worktree, err := clone(ctx, config, opts, decrypted, dir, progress) + if err != nil { + if err := os.RemoveAll(dir); err != nil { + return nil, fmt.Errorf("remove temp clone dir after clone failed: %w", err) + } + + return nil, fmt.Errorf("clone: %w", err) + } + + return &GoGitRepo{ + config: config, + opts: opts, + tree: worktree, + decryptedPassword: string(decrypted), + repo: repo, + dir: dir, + }, nil +} + +func clone(ctx context.Context, config *provisioning.Repository, opts GoGitCloneOptions, decrypted []byte, dir string, progress io.Writer) (*git.Repository, *git.Worktree, error) { + gitcfg := config.Spec.GitHub + url := fmt.Sprintf("%s.git", gitcfg.URL) + + branch := plumbing.NewBranchReferenceName(gitcfg.Branch) + cloneOpts := &git.CloneOptions{ + ReferenceName: branch, + Auth: &githttp.BasicAuth{ + Username: "grafana", // this can be anything except an empty string for PAT + Password: string(decrypted), // TODO... will need to get from a service! + }, + URL: url, + Progress: progress, + } + + repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts) + if errors.Is(err, plumbing.ErrReferenceNotFound) && opts.CreateIfNotExists { + cloneOpts.ReferenceName = "" // empty + repo, err = git.PlainCloneContext(ctx, dir, false, cloneOpts) + if err == nil { + worktree, err := repo.Worktree() + if err != nil { + return nil, nil, err + } + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: branch, + Force: true, + Create: true, + }) + if err != nil { + return nil, nil, fmt.Errorf("unable to create new branch: %w", err) + } + } + } else if err != nil { + return nil, nil, fmt.Errorf("clone error: %w", err) + } + + rcfg, err := repo.Config() + if err != nil { + return nil, nil, fmt.Errorf("error reading repository config %w", err) + } + + origin := rcfg.Remotes["origin"] + if origin == nil { + return nil, nil, fmt.Errorf("missing origin remote %w", err) + } + + if url != origin.URLs[0] { + return nil, nil, fmt.Errorf("unexpected remote (expected: %s, found: %s)", url, origin.URLs[0]) + } + + worktree, err := repo.Worktree() + if err != nil { + return nil, nil, fmt.Errorf("get worktree: %w", err) + } + + return repo, worktree, nil +} + +func mkdirTempClone(root string, config *provisioning.Repository) (string, error) { + if config.Namespace == "" { + return "", fmt.Errorf("config is missing namespace") + } + if config.Name == "" { + return "", fmt.Errorf("config is missing name") + } + return os.MkdirTemp(root, fmt.Sprintf("clone-%s-%s-", config.Namespace, config.Name)) +} + +// Affer making changes to the worktree, push changes +func (g *GoGitRepo) Push(ctx context.Context, opts GoGitPushOptions, progress io.Writer) error { + timeout := maxOperationTimeout + if opts.Timeout > 0 { + timeout = opts.Timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + if g.opts.SingleCommitBeforePush { + _, err := g.tree.Commit("exported from grafana", &git.CommitOptions{ + All: true, // Add everything that changed + }) + if err != nil { + // empty commit is fine -- no change + if !errors.Is(err, git.ErrEmptyCommit) { + return err + } + } + } + + err := g.repo.PushContext(ctx, &git.PushOptions{ + Progress: progress, + Force: true, // avoid fast-forward-errors + Auth: &githttp.BasicAuth{ // reuse logic from clone? + Username: "grafana", + Password: g.decryptedPassword, + }, + }) + if errors.Is(err, git.NoErrAlreadyUpToDate) { + return nil // same as the target + } + return err +} + +func (g *GoGitRepo) Remove(ctx context.Context) error { + return os.RemoveAll(g.dir) +} + +// Config implements repository.Repository. +func (g *GoGitRepo) Config() *provisioning.Repository { + return g.config +} + +// ReadTree implements repository.Repository. +func (g *GoGitRepo) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { + var treePath string + if g.config.Spec.GitHub.Path != "" { + treePath = g.config.Spec.GitHub.Path + } + + // TODO: do we really need this? + if !strings.HasPrefix(treePath, "/") { + treePath = "/" + treePath + } + + entries := make([]repository.FileTreeEntry, 0, 100) + err := util.Walk(g.tree.Filesystem, treePath, func(path string, info fs.FileInfo, err error) error { + if err != nil || path == "/" { + return err + } + entry := repository.FileTreeEntry{ + Path: strings.TrimLeft(path, "/"), + Size: info.Size(), + } + if !info.IsDir() { + entry.Blob = true + // For a real instance, this will likely be based on: + // https://github.com/go-git/go-git/blob/main/_examples/ls/main.go#L25 + entry.Hash = fmt.Sprintf("TODO/%d", info.Size()) // but not used for + } + entries = append(entries, entry) + return err + }) + if errors.Is(err, fs.ErrNotExist) { + // We intentionally ignore this case, as + } else if err != nil { + return nil, fmt.Errorf("failed to walk tree for ref '%s': %w", ref, err) + } + return entries, nil +} + +func (g *GoGitRepo) Test(ctx context.Context) (*provisioning.TestResults, error) { + return &provisioning.TestResults{ + Success: g.tree != nil, + }, nil +} + +// Update implements repository.Repository. +func (g *GoGitRepo) Update(ctx context.Context, path string, ref string, data []byte, message string) error { + return g.Write(ctx, path, ref, data, message) +} + +// Create implements repository.Repository. +func (g *GoGitRepo) Create(ctx context.Context, path string, ref string, data []byte, message string) error { + return g.Write(ctx, path, ref, data, message) +} + +// Write implements repository.Repository. +func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []byte, message string) error { + fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath) + if err := verifyPathWithoutRef(fpath, ref); err != nil { + return err + } + + // For folders, just create the folder and ignore the commit + if safepath.IsDir(fpath) { + return g.tree.Filesystem.MkdirAll(fpath, 0750) + } + + dir := safepath.Dir(fpath) + if dir != "" { + err := g.tree.Filesystem.MkdirAll(dir, 0750) + if err != nil { + return err + } + } + + file, err := g.tree.Filesystem.Create(fpath) + if err != nil { + return err + } + _, err = file.Write(data) + if err != nil { + return err + } + + _, err = g.tree.Add(fpath) + if err != nil { + return err + } + + // Skip commit for each file + if g.opts.SingleCommitBeforePush { + return nil + } + + opts := &git.CommitOptions{} + sig := repository.GetAuthorSignature(ctx) + if sig != nil { + opts.Author = &object.Signature{ + Name: sig.Name, + Email: sig.Email, + When: sig.When, + } + } + _, err = g.tree.Commit(message, opts) + if errors.Is(err, git.ErrEmptyCommit) { + return nil // empty commit is fine -- no change + } + return err +} + +// Delete implements repository.Repository. +func (g *GoGitRepo) Delete(ctx context.Context, path string, ref string, message string) error { + if _, err := g.tree.Remove(safepath.Join(g.config.Spec.GitHub.Path, path)); err != nil { + return err + } + + return nil +} + +// Read implements repository.Repository. +func (g *GoGitRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { + readPath := safepath.Join(g.config.Spec.GitHub.Path, path) + stat, err := g.tree.Filesystem.Lstat(readPath) + if err != nil { + return nil, fmt.Errorf("failed to stat path '%s': %w", readPath, err) + } + info := &repository.FileInfo{ + Path: path, + Modified: &metav1.Time{ + Time: stat.ModTime(), + }, + } + if !stat.IsDir() { + f, err := g.tree.Filesystem.Open(readPath) + if err != nil { + return nil, err + } + info.Data, err = io.ReadAll(f) + if err != nil { + return nil, err + } + } + return info, err +} + +func verifyPathWithoutRef(path string, ref string) error { + if path == "" { + return fmt.Errorf("expected path") + } + if ref != "" { + return fmt.Errorf("ref unsupported") + } + return nil +} + +// History implements repository.Repository. +func (g *GoGitRepo) History(ctx context.Context, path string, ref string) ([]provisioning.HistoryItem, error) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "history is not yet implemented", + Code: http.StatusNotImplemented, + }, + } +} + +// Validate implements repository.Repository. +func (g *GoGitRepo) Validate() field.ErrorList { + return nil +} + +// Webhook implements repository.Repository. +func (g *GoGitRepo) Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "history is not yet implemented", + Code: http.StatusNotImplemented, + }, + } +} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go new file mode 100644 index 00000000000..4ce86d18859 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go @@ -0,0 +1,94 @@ +package gogit + +import ( + "context" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +type dummySecret struct{} + +// Decrypt implements secrets.Service. +func (d *dummySecret) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + token, ok := os.LookupEnv("gitwraptoken") + if !ok { + return nil, fmt.Errorf("missing token in environment") + } + return []byte(token), nil +} + +// Encrypt implements secrets.Service. +func (d *dummySecret) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + panic("unimplemented") +} + +// FIXME!! NOTE!!!!! +// This is really just a sketchpad while trying to get things working +// the test makes destructive changes to a real git repository :) +// this should be removed before committing to main (likely sooner) +// and replaced with integration tests that check the more specific results +func TestGoGitWrapper(t *testing.T) { + _, ok := os.LookupEnv("gitwraptoken") + if !ok { + t.Skipf("no token found in environment") + } + + ctx := context.Background() + wrap, err := Clone(ctx, &v0alpha1.Repository{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "ns", + Name: "unit-tester", + }, + Spec: v0alpha1.RepositorySpec{ + GitHub: &v0alpha1.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/git-ui-sync-demo", + Branch: "ryan-test", + }, + }, + }, + GoGitCloneOptions{ + Root: "testdata/clone", // where things are cloned, + // one commit (not 11) + SingleCommitBeforePush: true, + CreateIfNotExists: true, + }, + &dummySecret{}, + os.Stdout) + require.NoError(t, err) + + tree, err := wrap.ReadTree(ctx, "") + require.NoError(t, err) + + jj, err := json.MarshalIndent(tree, "", " ") + require.NoError(t, err) + + fmt.Printf("TREE:%s\n", string(jj)) + + ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ + Name: "xxxxx", + Email: "rrr@yyyy.zzz", + When: time.Now(), + }) + + for i := 0; i < 10; i++ { + fname := fmt.Sprintf("deep/path/in/test_%d.txt", i) + fmt.Printf("Write:%s\n", fname) + err = wrap.Write(ctx, fname, "", []byte(fmt.Sprintf("body/%d %s", i, time.Now())), "the commit message") + require.NoError(t, err) + } + + fmt.Printf("push...\n") + err = wrap.Push(ctx, GoGitPushOptions{ + Timeout: 10, + }, os.Stdout) + require.NoError(t, err) +} diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local.go new file mode 100644 index 00000000000..42f568311d6 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/local.go @@ -0,0 +1,383 @@ +package repository + +import ( + "context" + "path" + + // Git still uses sha1 for the most part: https://git-scm.com/docs/hash-function-transition + //nolint:gosec + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +type LocalFolderResolver struct { + PermittedPrefixes []string + HomePath string +} + +type InvalidLocalFolderError struct { + Path string + AdditionalInfo string +} + +var ( + _ error = (*InvalidLocalFolderError)(nil) + _ apierrors.APIStatus = (*InvalidLocalFolderError)(nil) +) + +func (e *InvalidLocalFolderError) Error() string { + return fmt.Sprintf("the path given ('%s') is invalid for a local repository (%s)", e.Path, e.AdditionalInfo) +} + +func (e *InvalidLocalFolderError) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusBadRequest, + Reason: metav1.StatusReasonBadRequest, + Message: e.Error(), + } +} + +func (r *LocalFolderResolver) LocalPath(p string) (string, error) { + if len(r.PermittedPrefixes) == 0 { + return "", &InvalidLocalFolderError{p, "no permitted prefixes were configured"} + } + + originalPath := p + if !path.IsAbs(p) { + p = safepath.Join(r.HomePath, p) + } else { + p = safepath.Clean(p) + } + + for _, permitted := range r.PermittedPrefixes { + if safepath.InDir(p, permitted) { + return p, nil + } + } + return "", &InvalidLocalFolderError{originalPath, "the path matches no permitted prefix"} +} + +var ( + _ Repository = (*localRepository)(nil) + _ Writer = (*localRepository)(nil) + _ Reader = (*localRepository)(nil) +) + +type localRepository struct { + config *provisioning.Repository + resolver *LocalFolderResolver + + // validated path that can be read if not empty + path string +} + +func NewLocal(config *provisioning.Repository, resolver *LocalFolderResolver) *localRepository { + r := &localRepository{ + config: config, + resolver: resolver, + } + if config.Spec.Local != nil { + r.path, _ = resolver.LocalPath(config.Spec.Local.Path) + if r.path != "" && !safepath.IsDir(r.path) { + r.path += "/" + } + + for i, permitted := range r.resolver.PermittedPrefixes { + r.resolver.PermittedPrefixes[i] = safepath.Clean(permitted) + } + } + + return r +} + +func (r *localRepository) Config() *provisioning.Repository { + return r.config +} + +// Validate implements provisioning.Repository. +func (r *localRepository) Validate() (fields field.ErrorList) { + cfg := r.config.Spec.Local + if cfg == nil { + fields = append(fields, &field.Error{ + Type: field.ErrorTypeRequired, + Field: "spec.local", + }) + return fields + } + + // The path value must be set for local provisioning + if cfg.Path == "" { + fields = append(fields, field.Required(field.NewPath("spec", "local", "path"), + "must enter a path to local file")) + } + + // Check if it is valid + _, err := r.resolver.LocalPath(cfg.Path) + if err != nil { + fields = append(fields, field.Invalid(field.NewPath("spec", "local", "path"), + cfg.Path, err.Error())) + } + + if err := safepath.IsSafe(cfg.Path); err != nil { + fields = append(fields, field.Invalid(field.NewPath("spec", "local", "path"), + cfg.Path, err.Error())) + } + + return fields +} + +// Test implements provisioning.Repository. +// NOTE: Validate has been called (and passed) before this function should be called +func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + if r.config.Spec.Local.Path == "" { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{ + "no path is configured", + }, + }, nil + } + + _, err := r.resolver.LocalPath(r.config.Spec.Local.Path) + if err != nil { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{ + err.Error(), + }, + }, nil + } + + _, err = os.Stat(r.path) + if errors.Is(err, os.ErrNotExist) { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{ + fmt.Sprintf("directory not found: %s", r.config.Spec.Local.Path), + }, + }, nil + } + + return &provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil +} + +// Test implements provisioning.Repository. +func (r *localRepository) validateRequest(ref string) error { + if ref != "" { + return apierrors.NewBadRequest("local repository does not support ref") + } + if r.path == "" { + _, err := r.resolver.LocalPath(r.config.Spec.Local.Path) + if err != nil { + return err + } + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "the service is missing a root path", + Code: http.StatusFailedDependency, + }, + } + } + return nil +} + +// ReadResource implements provisioning.Repository. +func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*FileInfo, error) { + if err := r.validateRequest(ref); err != nil { + return nil, err + } + + actualPath := safepath.Join(r.path, filePath) + info, err := os.Stat(actualPath) + if errors.Is(err, os.ErrNotExist) { + return nil, ErrFileNotFound + } else if err != nil { + return nil, fmt.Errorf("stat file: %w", err) + } + + if info.IsDir() { + return &FileInfo{ + Path: filePath, + Modified: &metav1.Time{ + Time: info.ModTime(), + }, + }, nil + } + + //nolint:gosec + data, err := os.ReadFile(actualPath) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + + hash, _, err := r.calculateFileHash(actualPath) + if err != nil { + return nil, fmt.Errorf("calculate hash of file: %w", err) + } + + return &FileInfo{ + Path: filePath, + Data: data, + Hash: hash, + Modified: &metav1.Time{ + Time: info.ModTime(), + }, + }, nil +} + +// ReadResource implements provisioning.Repository. +func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { + if err := r.validateRequest(ref); err != nil { + return nil, err + } + + // Return an empty list when folder does not exist + _, err := os.Stat(r.path) + if errors.Is(err, fs.ErrNotExist) { + return []FileTreeEntry{}, nil + } + + rootlen := len(r.path) + entries := make([]FileTreeEntry, 0, 100) + err = filepath.Walk(r.path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + entry := FileTreeEntry{ + Path: strings.TrimLeft(path[rootlen:], "/"), + Size: info.Size(), + } + if !info.IsDir() { + entry.Blob = true + entry.Hash, _, err = r.calculateFileHash(path) + if err != nil { + return fmt.Errorf("failed to read and calculate hash of path %s: %w", path, err) + } + } + // TODO: do folders have a trailing slash? + entries = append(entries, entry) + return err + }) + + return entries, err +} + +func (r *localRepository) calculateFileHash(path string) (string, int64, error) { + // Treats https://securego.io/docs/rules/g304.html + if !safepath.InDir(path, r.path) { + return "", 0, ErrFileNotFound + } + + // We've already made sure the path is safe, so we'll ignore the gosec lint. + //nolint:gosec + file, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return "", 0, err + } + + // TODO: Define what hashing algorithm we want to use for the entire repository. Maybe a config option? + hasher := sha1.New() + // TODO: context-aware io.Copy? Is that even possible with a reasonable impl? + size, err := io.Copy(hasher, file) + if err != nil { + return "", 0, err + } + // NOTE: EncodeToString (& hex.Encode for that matter) return lower-case hex. + return hex.EncodeToString(hasher.Sum(nil)), size, nil +} + +func (r *localRepository) Create(ctx context.Context, fpath string, ref string, data []byte, comment string) error { + if err := r.validateRequest(ref); err != nil { + return err + } + + fpath = safepath.Join(r.path, fpath) + _, err := os.Stat(fpath) + if !errors.Is(err, os.ErrNotExist) { + if err != nil { + return apierrors.NewInternalError(fmt.Errorf("failed to check if file exists: %w", err)) + } + return apierrors.NewAlreadyExists(provisioning.RepositoryResourceInfo.GroupResource(), fpath) + } + + if safepath.IsDir(fpath) { + if data != nil { + return apierrors.NewBadRequest("data cannot be provided for a directory") + } + + if err := os.MkdirAll(fpath, 0700); err != nil { + return apierrors.NewInternalError(fmt.Errorf("failed to create path: %w", err)) + } + + return nil + } + + if err := os.MkdirAll(path.Dir(fpath), 0700); err != nil { + return apierrors.NewInternalError(fmt.Errorf("failed to create path: %w", err)) + } + + return os.WriteFile(fpath, data, 0600) +} + +func (r *localRepository) Update(ctx context.Context, path string, ref string, data []byte, comment string) error { + if err := r.validateRequest(ref); err != nil { + return err + } + + path = safepath.Join(r.path, path) + if safepath.IsDir(path) { + return apierrors.NewBadRequest("cannot update a directory") + } + + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("file does not exist") + } + return os.WriteFile(path, data, 0600) +} + +func (r *localRepository) Write(ctx context.Context, fpath, ref string, data []byte, comment string) error { + if err := r.validateRequest(ref); err != nil { + return err + } + + fpath = safepath.Join(r.path, fpath) + if safepath.IsDir(fpath) { + return os.MkdirAll(fpath, 0700) + } + + if err := os.MkdirAll(path.Dir(fpath), 0700); err != nil { + return apierrors.NewInternalError(fmt.Errorf("failed to create path: %w", err)) + } + + return os.WriteFile(fpath, data, 0600) +} + +func (r *localRepository) Delete(ctx context.Context, path string, ref string, comment string) error { + if err := r.validateRequest(ref); err != nil { + return err + } + + return os.Remove(safepath.Join(r.path, path)) +} diff --git a/pkg/registry/apis/provisioning/repository/local_test.go b/pkg/registry/apis/provisioning/repository/local_test.go new file mode 100644 index 00000000000..0052582856c --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/local_test.go @@ -0,0 +1,86 @@ +package repository + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +func TestLocalResolver(t *testing.T) { + resolver := &LocalFolderResolver{ + PermittedPrefixes: []string{ + "/github/testdata", + }, + HomePath: "/", + } + + _, err := resolver.LocalPath("github/testdata") + require.NoError(t, err) + + _, err = resolver.LocalPath("something") + require.Error(t, err) +} + +func TestLocal(t *testing.T) { + // Valid paths test cases + for _, tc := range []struct { + Name string + Path string + PermittedPrefixes []string + ExpectedPath string + }{ + {"relative path", "devenv/test", []string{"/home/grafana"}, "/home/grafana/devenv/test/"}, + {"absolute path", "/devenv/test", []string{"/devenv"}, "/devenv/test/"}, + {"relative path with multiple prefixes", "devenv/test", []string{"/home/grafana", "/devenv"}, "/home/grafana/devenv/test/"}, + {"absolute path with multiple prefixes", "/devenv/test", []string{"/home/grafana", "/devenv"}, "/devenv/test/"}, + } { + t.Run("valid: "+tc.Name, func(t *testing.T) { + r := NewLocal(&v0alpha1.Repository{ + Spec: v0alpha1.RepositorySpec{ + Local: &v0alpha1.LocalRepositoryConfig{ + Path: tc.Path, + }, + }, + }, &LocalFolderResolver{PermittedPrefixes: tc.PermittedPrefixes, HomePath: "/home/grafana"}) + + assert.Equal(t, tc.ExpectedPath, r.path, "expected path to be resolved") + for _, err := range r.Validate() { + assert.Fail(t, "unexpected validation failure", "unexpected validation error on field %s: %s", err.Field, err.ErrorBody()) + } + }) + } + + // Invalid paths test cases + for _, tc := range []struct { + Name string + Path string + PermittedPrefixes []string + }{ + {"no configured paths", "invalid/path", nil}, + {"path traversal escape", "../../etc/passwd", []string{"/home/grafana"}}, + {"unconfigured prefix", "invalid/path", []string{"devenv", "/tmp", "test"}}, + } { + t.Run("invalid: "+tc.Name, func(t *testing.T) { + r := NewLocal(&v0alpha1.Repository{ + Spec: v0alpha1.RepositorySpec{ + Local: &v0alpha1.LocalRepositoryConfig{ + Path: tc.Path, + }, + }, + }, &LocalFolderResolver{PermittedPrefixes: tc.PermittedPrefixes, HomePath: "/home/grafana"}) + + require.Empty(t, r.path, "no path should be resolved") + + errs := r.Validate() + require.NotEmpty(t, errs, "expected validation errors") + for _, err := range errs { + if !assert.Equal(t, "spec.local.path", err.Field) { + assert.FailNow(t, "unexpected validation failure", "unexpected validation error on field %s: %s", err.Field, err.ErrorBody()) + } + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index d4dd2aee63d..d9d3bc9a4ec 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -2,6 +2,8 @@ package repository import ( "context" + "errors" + "fmt" "io/fs" "net/http" @@ -59,6 +61,8 @@ type FileTreeEntry struct { } type Reader interface { + Repository + // Read a file from the resource // This data will be parsed and validated before it is shown to end users Read(ctx context.Context, path, ref string) (*FileInfo, error) @@ -72,6 +76,8 @@ type Reader interface { } type Writer interface { + Repository + // Write a file to the repository. // The data has already been validated and is ready for save Create(ctx context.Context, path, ref string, data []byte, message string) error @@ -88,8 +94,23 @@ type Writer interface { Delete(ctx context.Context, path, ref, message string) error } +type ReaderWriter interface { + Reader + Writer +} + +// Hooks called after the repository has been created, updated or deleted +type RepositoryWithURLs interface { + Repository + + // Get resource URLs for a file inside a repository + ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) +} + // Hooks called after the repository has been created, updated or deleted type Hooks interface { + Repository + // For repositories that support webhooks Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error) OnCreate(ctx context.Context) (*provisioning.WebhookStatus, error) @@ -126,3 +147,14 @@ type Versioned interface { LatestRef(ctx context.Context) (string, error) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) } + +func writeWithReadThenCreateOrUpdate(ctx context.Context, r ReaderWriter, path, ref string, data []byte, comment string) error { + _, err := r.Read(ctx, path, ref) + if err != nil && !(errors.Is(err, ErrFileNotFound)) { + return fmt.Errorf("failed to check if file exists before writing: %w", err) + } + if err == nil { + return r.Update(ctx, path, ref, data, comment) + } + return r.Create(ctx, path, ref, data, comment) +} diff --git a/pkg/registry/apis/provisioning/repository/repository_mock.go b/pkg/registry/apis/provisioning/repository/repository_mock.go new file mode 100644 index 00000000000..b7f5bda049b --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/repository_mock.go @@ -0,0 +1,295 @@ +// Code generated by mockery v2.50.0. DO NOT EDIT. + +package repository + +import ( + context "context" + http "net/http" + + field "k8s.io/apimachinery/pkg/util/validation/field" + + mock "github.com/stretchr/testify/mock" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +// MockRepository is an autogenerated mock type for the Repository type +type MockRepository struct { + mock.Mock +} + +// Config provides a mock function with no fields +func (_m *MockRepository) Config() *v0alpha1.Repository { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Config") + } + + var r0 *v0alpha1.Repository + if rf, ok := ret.Get(0).(func() *v0alpha1.Repository); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.Repository) + } + } + + return r0 +} + +// Create provides a mock function with given fields: ctx, path, ref, data, message +func (_m *MockRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error { + ret := _m.Called(ctx, path, ref, data, message) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok { + r0 = rf(ctx, path, ref, data, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: ctx, path, ref, message +func (_m *MockRepository) Delete(ctx context.Context, path string, ref string, message string) error { + ret := _m.Called(ctx, path, ref, message) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, path, ref, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// History provides a mock function with given fields: ctx, path, ref +func (_m *MockRepository) History(ctx context.Context, path string, ref string) ([]v0alpha1.HistoryItem, error) { + ret := _m.Called(ctx, path, ref) + + if len(ret) == 0 { + panic("no return value specified for History") + } + + var r0 []v0alpha1.HistoryItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]v0alpha1.HistoryItem, error)); ok { + return rf(ctx, path, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []v0alpha1.HistoryItem); ok { + r0 = rf(ctx, path, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v0alpha1.HistoryItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, path, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Read provides a mock function with given fields: ctx, path, ref +func (_m *MockRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { + ret := _m.Called(ctx, path, ref) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 *FileInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok { + return rf(ctx, path, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok { + r0 = rf(ctx, path, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*FileInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, path, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReadTree provides a mock function with given fields: ctx, ref +func (_m *MockRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { + ret := _m.Called(ctx, ref) + + if len(ret) == 0 { + panic("no return value specified for ReadTree") + } + + var r0 []FileTreeEntry + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok { + return rf(ctx, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok { + r0 = rf(ctx, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]FileTreeEntry) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Test provides a mock function with given fields: ctx +func (_m *MockRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Test") + } + + var r0 *v0alpha1.TestResults + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*v0alpha1.TestResults, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *v0alpha1.TestResults); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.TestResults) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, path, ref, data, message +func (_m *MockRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error { + ret := _m.Called(ctx, path, ref, data, message) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok { + r0 = rf(ctx, path, ref, data, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Validate provides a mock function with no fields +func (_m *MockRepository) Validate() field.ErrorList { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Validate") + } + + var r0 field.ErrorList + if rf, ok := ret.Get(0).(func() field.ErrorList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(field.ErrorList) + } + } + + return r0 +} + +// Webhook provides a mock function with given fields: ctx, req +func (_m *MockRepository) Webhook(ctx context.Context, req *http.Request) (*v0alpha1.WebhookResponse, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Webhook") + } + + var r0 *v0alpha1.WebhookResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *http.Request) (*v0alpha1.WebhookResponse, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *http.Request) *v0alpha1.WebhookResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.WebhookResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *http.Request) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Write provides a mock function with given fields: ctx, path, ref, data, message +func (_m *MockRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { + ret := _m.Called(ctx, path, ref, data, message) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok { + r0 = rf(ctx, path, ref, data, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepository { + mock := &MockRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/test.go b/pkg/registry/apis/provisioning/repository/test.go new file mode 100644 index 00000000000..8ba064ea08c --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/test.go @@ -0,0 +1,87 @@ +package repository + +import ( + "context" + "fmt" + "net/http" + "slices" + + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +// Tester is a struct that implements the Tester interface +// it's temporary +// FIXME: remove as soon as controller and jobs refactoring PRs are merged +type Tester struct{} + +func (t *Tester) TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { + return TestRepository(ctx, repo) +} + +func TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { + errors := ValidateRepository(repo) + if len(errors) > 0 { + rsp := &provisioning.TestResults{ + Code: http.StatusUnprocessableEntity, // Invalid + Success: false, + Errors: make([]string, len(errors)), + } + for i, v := range errors { + rsp.Errors[i] = v.Error() + } + return rsp, nil + } + + return repo.Test(ctx) +} + +func ValidateRepository(repo Repository) field.ErrorList { + list := repo.Validate() + cfg := repo.Config() + + if cfg.Spec.Title == "" { + list = append(list, field.Required(field.NewPath("spec", "title"), "a repository title must be given")) + } + + if cfg.Spec.Sync.Enabled && cfg.Spec.Sync.Target == "" { + list = append(list, field.Required(field.NewPath("spec", "sync", "target"), + "The target type is required when sync is enabled")) + } + + if cfg.Spec.Sync.Enabled && cfg.Spec.Sync.IntervalSeconds < 10 { + list = append(list, field.Invalid(field.NewPath("spec", "sync", "intervalSeconds"), + cfg.Spec.Sync.IntervalSeconds, fmt.Sprintf("Interval must be at least %d seconds", 10))) + } + + // Reserved names (for now) + reserved := []string{"classic", "sql", "SQL", "plugins", "legacy", "new", "job", "github", "s3", "gcs", "file", "new", "create", "update", "delete"} + if slices.Contains(reserved, cfg.Name) { + list = append(list, field.Invalid(field.NewPath("metadata", "name"), cfg.Name, "Name is reserved, choose a different identifier")) + } + + if cfg.Spec.Type != provisioning.LocalRepositoryType && cfg.Spec.Local != nil { + list = append(list, field.Invalid(field.NewPath("spec", "local"), + cfg.Spec.GitHub, "Local config only valid when type is local")) + } + + if cfg.Spec.Type != provisioning.GitHubRepositoryType && cfg.Spec.GitHub != nil { + list = append(list, field.Invalid(field.NewPath("spec", "github"), + cfg.Spec.GitHub, "Github config only valid when type is github")) + } + + for _, w := range cfg.Spec.Workflows { + switch w { + case provisioning.WriteWorkflow: // valid; no fall thru + case provisioning.BranchWorkflow: + if cfg.Spec.Type != provisioning.GitHubRepositoryType { + list = append(list, field.Invalid(field.NewPath("spec", "workflow"), w, "branch is only supported on git repositories")) + } + default: + list = append(list, field.Invalid(field.NewPath("spec", "workflow"), w, "invalid workflow")) + } + } + + return list +} diff --git a/pkg/registry/apis/provisioning/repository/test_test.go b/pkg/registry/apis/provisioning/repository/test_test.go new file mode 100644 index 00000000000..4e7b726ea56 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/test_test.go @@ -0,0 +1,315 @@ +package repository + +import ( + "context" + "fmt" + "net/http" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestValidateRepository(t *testing.T) { + tests := []struct { + name string + repository *MockRepository + expectedErrs int + validateError func(t *testing.T, errors field.ErrorList) + }{ + { + name: "valid repository", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 0, + }, + { + name: "missing title", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{}, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.title: Required value") + }, + }, + { + name: "sync enabled without target", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Sync: provisioning.SyncOptions{ + Enabled: true, + IntervalSeconds: 10, + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.sync.target: Required value") + }, + }, + { + name: "sync interval too low", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Sync: provisioning.SyncOptions{ + Enabled: true, + Target: "test", + IntervalSeconds: 5, + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.sync.intervalSeconds: Invalid value") + }, + }, + { + name: "reserved name", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sql", + }, + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "metadata.name: Invalid value") + }, + }, + { + name: "mismatched local config", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Type: provisioning.GitHubRepositoryType, + Local: &provisioning.LocalRepositoryConfig{}, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.local: Invalid value") + }, + }, + { + name: "mismatched github config", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Type: provisioning.LocalRepositoryType, + GitHub: &provisioning.GitHubRepositoryConfig{}, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.github: Invalid value") + }, + }, + { + name: "multiple validation errors", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sql", + }, + Spec: provisioning.RepositorySpec{ + Sync: provisioning.SyncOptions{ + Enabled: true, + IntervalSeconds: 5, + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 4, // Updated from 3 to 4 to match actual errors: + // 1. missing title + // 2. sync target missing + // 3. sync interval too low + // 4. reserved name + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errors := ValidateRepository(tt.repository) + require.Len(t, errors, tt.expectedErrs) + if tt.validateError != nil { + tt.validateError(t, errors) + } + }) + } +} + +func TestTestRepository(t *testing.T) { + tests := []struct { + name string + repository *MockRepository + expectedCode int + expectedErrs []string + expectedError error + }{ + { + name: "validation fails", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + // Missing required title + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedCode: http.StatusUnprocessableEntity, + expectedErrs: []string{"spec.title: Required value: a repository title must be given"}, + }, + { + name: "test passes", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + return m + }(), + expectedCode: http.StatusOK, + expectedErrs: nil, + }, + { + name: "test fails with error", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(nil, fmt.Errorf("test error")) + return m + }(), + expectedError: fmt.Errorf("test error"), + }, + { + name: "test fails with results", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []string{"test failed"}, + }, nil) + return m + }(), + expectedCode: http.StatusBadRequest, + expectedErrs: []string{"test failed"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, err := TestRepository(context.Background(), tt.repository) + + if tt.expectedError != nil { + require.Error(t, err) + require.Equal(t, tt.expectedError.Error(), err.Error()) + return + } + + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, tt.expectedCode, results.Code) + + if tt.expectedErrs != nil { + require.Equal(t, tt.expectedErrs, results.Errors) + require.False(t, results.Success) + } else { + require.True(t, results.Success) + require.Empty(t, results.Errors) + } + }) + } +} + +func TestTester_TestRepository(t *testing.T) { + tester := &Tester{} + + // Test that it properly delegates to TestRepository + repository := NewMockRepository(t) + repository.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + repository.On("Validate").Return(field.ErrorList{}) + repository.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + + results, err := tester.TestRepository(context.Background(), repository) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, http.StatusOK, results.Code) + require.True(t, results.Success) +} diff --git a/pkg/registry/apis/provisioning/repository/workflows.go b/pkg/registry/apis/provisioning/repository/workflows.go new file mode 100644 index 00000000000..2b98f2b438a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/workflows.go @@ -0,0 +1,40 @@ +package repository + +import ( + "net/http" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +func IsWriteAllowed(repo *provisioning.Repository, ref string) error { + if len(repo.Spec.Workflows) == 0 { + return apierrors.NewBadRequest("this repository is read only") + } + + if ref == "" { + for _, v := range repo.Spec.Workflows { + if v == provisioning.WriteWorkflow { + return nil // found + } + } + return apierrors.NewBadRequest("this repository does not support the write workflow") + } + + // Only github + if repo.Spec.Type != provisioning.GitHubRepositoryType { + return &apierrors.StatusError{ErrStatus: v1.Status{ + Code: http.StatusPreconditionFailed, + Message: "Only github supports writing to a branch", + }} + } + + for _, v := range repo.Spec.Workflows { + if v == provisioning.BranchWorkflow { + return nil + } + } + return apierrors.NewBadRequest("this repository does not support the branch workflow") +} diff --git a/pkg/registry/apis/provisioning/request.go b/pkg/registry/apis/provisioning/request.go new file mode 100644 index 00000000000..616c86538f2 --- /dev/null +++ b/pkg/registry/apis/provisioning/request.go @@ -0,0 +1,69 @@ +package provisioning + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +const ( + // contentTypeJSON is the Content-Type for JSON requests as go standard library does not provide one + contentTypeJSON = "application/json" + // defaultMaxBodySize is the default max size for request bodies (10KB) + defaultMaxBodySize = 10 * 1024 + // errMsgRequestTooLarge is the error message for request bodies that are too large + errMsgRequestTooLarge = "request body too large" +) + +// readBody reads the request body and limits the size +func readBody(r *http.Request, maxSize int64) ([]byte, error) { + limitedBody := http.MaxBytesReader(nil, r.Body, maxSize) + body, err := io.ReadAll(limitedBody) + if err != nil { + var maxBytesError *http.MaxBytesError + if errors.As(err, &maxBytesError) { + return nil, fmt.Errorf("%s: max size %d bytes", errMsgRequestTooLarge, maxSize) + } + return nil, fmt.Errorf("error reading request body: %w", err) + } + + return body, nil +} + +// isJSONContentType checks if the request has the JSON Content-Type +func isJSONContentType(r *http.Request) bool { + contentType := r.Header.Get("Content-Type") + return strings.HasPrefix(contentType, contentTypeJSON) +} + +// unmarshalJSON unmarshals the request body into the provided interface +// it also checks the Content-Type and limits the size of the request body +func unmarshalJSON(r *http.Request, maxSize int64, v interface{}) error { + if !isJSONContentType(r) { + return fmt.Errorf("content type is not JSON: %s", r.Header.Get("Content-Type")) + } + + r.Body = http.MaxBytesReader(nil, r.Body, maxSize) + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + if err := decoder.Decode(v); err != nil { + var maxBytesError *http.MaxBytesError + if errors.As(err, &maxBytesError) { + return fmt.Errorf("%s: max size %d bytes", errMsgRequestTooLarge, maxSize) + } + if err == io.EOF { + return fmt.Errorf("empty request body") + } + return fmt.Errorf("error decoding JSON: %w", err) + } + + if decoder.More() { + return fmt.Errorf("multiple JSON objects not allowed") + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/request_test.go b/pkg/registry/apis/provisioning/request_test.go new file mode 100644 index 00000000000..3fedd8d550b --- /dev/null +++ b/pkg/registry/apis/provisioning/request_test.go @@ -0,0 +1,176 @@ +package provisioning + +import ( + "net/http/httptest" + "strings" + "testing" +) + +func TestReadBody(t *testing.T) { + tests := []struct { + name string + body string + maxSize int64 + wantErr bool + errContains string + }{ + { + name: "valid small body", + body: "hello", + maxSize: 10, + }, + { + name: "body too large", + body: "this is a very long body that exceeds the limit", + maxSize: 10, + wantErr: true, + errContains: errMsgRequestTooLarge, + }, + { + name: "body exactly at limit", + body: "1234567890", + maxSize: 10, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("POST", "/", strings.NewReader(tt.body)) + got, err := readBody(r, tt.maxSize) + + if tt.wantErr { + if err == nil { + t.Error("readBody() expected error but got none") + } + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("readBody() error = %v, want containing %v", err, tt.errContains) + } + return + } + if err != nil { + t.Errorf("readBody() unexpected error = %v", err) + return + } + if string(got) != tt.body { + t.Errorf("readBody() = %v, want %v", string(got), tt.body) + } + }) + } +} + +func TestIsJSONContentType(t *testing.T) { + tests := []struct { + name string + contentType string + want bool + }{ + { + name: "valid JSON content type", + contentType: "application/json", + want: true, + }, + { + name: "JSON with charset", + contentType: "application/json; charset=utf-8", + want: true, + }, + { + name: "not JSON", + contentType: "text/plain", + want: false, + }, + { + name: "empty content type", + contentType: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("POST", "/", nil) + r.Header.Set("Content-Type", tt.contentType) + if got := isJSONContentType(r); got != tt.want { + t.Errorf("isJSONContentType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnmarshalJSON(t *testing.T) { + type testStruct struct { + Name string `json:"name"` + Age int `json:"age"` + } + + tests := []struct { + name string + body string + maxSize int64 + contentType string + wantErr bool + errContains string + }{ + { + name: "valid JSON", + body: `{"name":"test","age":30}`, + maxSize: 1024, + contentType: contentTypeJSON, + }, + { + name: "invalid content type", + body: `{"name":"test"}`, + maxSize: 1024, + contentType: "text/plain", + wantErr: true, + errContains: "content type is not JSON", + }, + { + name: "body too large", + body: `{"name":"test","age":30}`, + maxSize: 10, + contentType: contentTypeJSON, + wantErr: true, + errContains: errMsgRequestTooLarge, + }, + { + name: "multiple JSON objects", + body: `{"name":"test"} {"name":"test2"}`, + maxSize: 1024, + contentType: contentTypeJSON, + wantErr: true, + errContains: "multiple JSON objects not allowed", + }, + { + name: "empty body", + body: "", + maxSize: 1024, + contentType: contentTypeJSON, + wantErr: true, + errContains: "empty request body", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("POST", "/", strings.NewReader(tt.body)) + r.Header.Set("Content-Type", tt.contentType) + + var result testStruct + err := unmarshalJSON(r, tt.maxSize, &result) + + if tt.wantErr { + if err == nil { + t.Error("unmarshalJSON() expected error but got none") + } + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("unmarshalJSON() error = %v, want containing %v", err, tt.errContains) + } + return + } + if err != nil { + t.Errorf("unmarshalJSON() unexpected error = %v", err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go new file mode 100644 index 00000000000..29d13db3b58 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -0,0 +1,167 @@ +package resources + +import ( + "context" + "fmt" + "sync" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/client" +) + +type ClientFactory struct { + configProvider apiserver.RestConfigProvider +} + +func NewClientFactory(configProvider apiserver.RestConfigProvider) *ClientFactory { + return &ClientFactory{configProvider} +} + +func (f *ClientFactory) Clients(ctx context.Context, namespace string) (*ResourceClients, error) { + restConfig, err := f.configProvider.GetRestConfig(ctx) + if err != nil { + return nil, err + } + + if namespace == "" { + return nil, fmt.Errorf("missing namespace") + } + + discovery, err := client.NewDiscoveryClient(restConfig) + if err != nil { + return nil, err + } + + client, err := dynamic.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return &ResourceClients{ + namespace: namespace, + discovery: discovery, + dynamic: client, + byKind: make(map[schema.GroupVersionKind]*clientInfo), + byResource: make(map[schema.GroupVersionResource]*clientInfo), + }, nil +} + +type ResourceClients struct { + namespace string + + dynamic dynamic.Interface + discovery client.DiscoveryClient + + // ResourceInterface cache for this context + namespace + mutex sync.Mutex + byKind map[schema.GroupVersionKind]*clientInfo + byResource map[schema.GroupVersionResource]*clientInfo +} + +type clientInfo struct { + gvk schema.GroupVersionKind + gvr schema.GroupVersionResource + client dynamic.ResourceInterface +} + +func (c *ResourceClients) ForKind(gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + info, ok := c.byKind[gvk] + if ok && info.client != nil { + return info.client, info.gvr, nil + } + + gvr, err := c.discovery.GetResourceForKind(gvk) + if err != nil { + return nil, schema.GroupVersionResource{}, err + } + info = &clientInfo{ + gvk: gvk, + gvr: gvr, + client: c.dynamic.Resource(gvr).Namespace(c.namespace), + } + c.byKind[gvk] = info + c.byResource[gvr] = info + return info.client, info.gvr, nil +} + +func (c *ResourceClients) ForResource(gvr schema.GroupVersionResource) (dynamic.ResourceInterface, schema.GroupVersionKind, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + info, ok := c.byResource[gvr] + if ok && info.client != nil { + return info.client, info.gvk, nil + } + + var err error + var gvk schema.GroupVersionKind + var versionless schema.GroupVersionResource + if gvr.Version == "" { + versionless = gvr + gvr, gvk, err = c.discovery.GetPreferredVesion(schema.GroupResource{ + Group: gvr.Group, + Resource: gvr.Resource, + }) + if err != nil { + return nil, schema.GroupVersionKind{}, err + } + + info, ok := c.byResource[gvr] + if ok && info.client != nil { + c.byResource[versionless] = info + return info.client, info.gvk, nil + } + } else { + gvk, err = c.discovery.GetKindForResource(gvr) + if err != nil { + return nil, schema.GroupVersionKind{}, err + } + } + info = &clientInfo{ + gvk: gvk, + gvr: gvr, + client: c.dynamic.Resource(gvr).Namespace(c.namespace), + } + c.byKind[gvk] = info + c.byResource[gvr] = info + if versionless.Group != "" { + c.byResource[versionless] = info + } + return info.client, info.gvk, nil +} + +func (c *ResourceClients) Folder() (dynamic.ResourceInterface, error) { + v, _, err := c.ForResource(schema.GroupVersionResource{ + Group: folders.GROUP, + Version: folders.VERSION, + Resource: folders.RESOURCE, + }) + return v, err +} + +func (c *ResourceClients) User() (dynamic.ResourceInterface, error) { + v, _, err := c.ForResource(schema.GroupVersionResource{ + Group: iam.GROUP, + Version: iam.VERSION, + Resource: iam.UserResourceInfo.GroupResource().Resource, + }) + return v, err +} + +func (c *ResourceClients) Dashboard() (dynamic.ResourceInterface, error) { + v, _, err := c.ForResource(schema.GroupVersionResource{ + Group: dashboard.GROUP, + Version: dashboard.VERSION, + Resource: dashboard.DASHBOARD_RESOURCE, + }) + return v, err +} diff --git a/pkg/registry/apis/provisioning/resources/fileformat.go b/pkg/registry/apis/provisioning/resources/fileformat.go new file mode 100644 index 00000000000..e2805954343 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/fileformat.go @@ -0,0 +1,107 @@ +package resources + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +var ErrUnableToReadResourceBytes = errors.New("unable to read bytes as a resource") +var ErrClassicResourceIsAlreadyK8sForm = errors.New("classic resource is already structured with apiVersion and kind") + +// This reads a "classic" file format and will convert it to an unstructured k8s resource +// The file path may determine how the resource is parsed +// +// The context and logger are both only used for logging purposes. They do not control any logic. +func ReadClassicResource(ctx context.Context, info *repository.FileInfo) (*unstructured.Unstructured, *schema.GroupVersionKind, provisioning.ClassicFileType, error) { + var value map[string]any + + // Try parsing as JSON + if info.Data[0] == '{' { + err := json.Unmarshal(info.Data, &value) + if err != nil { + return nil, nil, "", err + } + } else { + return nil, nil, "", fmt.Errorf("yaml not yet implemented") + } + + // regular version headers exist + // TODO: do we intend on this checking Kind or kind? document reasoning. + if value["apiVersion"] != nil { + if value["kind"] != nil { + return nil, nil, "", ErrClassicResourceIsAlreadyK8sForm + } + + logging.FromContext(ctx).Debug("TODO... likely a provisioning", + "apiVersion", value["apiVersion"], + "kind", value["Kind"]) + gv, err := schema.ParseGroupVersion(value["apiVersion"].(string)) + if err != nil { + return nil, nil, "", fmt.Errorf("invalid apiVersion") + } + gvk := gv.WithKind(value["Kind"].(string)) + return &unstructured.Unstructured{Object: value}, &gvk, "", nil + } + + // If this is a dashboard, convert it + if value["panels"] != nil && + value["schemaVersion"] != nil && + value["tags"] != nil { + gvk := &schema.GroupVersionKind{ + Group: dashboard.GROUP, + Version: dashboard.VERSION, // v1 + Kind: "Dashboard"} + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": gvk.GroupVersion().String(), + "kind": gvk.Kind, + "metadata": map[string]any{ + "name": value["uid"], + }, + "spec": value, + }, + }, gvk, provisioning.ClassicDashboard, nil + } + + return nil, nil, "", ErrUnableToReadResourceBytes +} + +// DecodeYAMLObject reads the input as YAML and outputs its Kubernetes resource, if it is one. +// Note that all JSON is also valid YAML, so this can also be used for JSON data. +func DecodeYAMLObject(input io.Reader) (*unstructured.Unstructured, *schema.GroupVersionKind, error) { + data, err := io.ReadAll(input) + if err != nil { + return nil, nil, err + } + + obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme). + Decode(data, nil, nil) + if err != nil { + return nil, gvk, err + } + + // The decoder should put it directly into an unstructured object + val, ok := obj.(*unstructured.Unstructured) + if ok { + return val, gvk, err + } + + unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, gvk, err + } + return &unstructured.Unstructured{Object: unstructuredMap}, gvk, err +} diff --git a/pkg/registry/apis/provisioning/resources/fileformat_test.go b/pkg/registry/apis/provisioning/resources/fileformat_test.go new file mode 100644 index 00000000000..925272dfa59 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/fileformat_test.go @@ -0,0 +1,118 @@ +package resources + +import ( + "bytes" + "context" + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +func TestUtils(t *testing.T) { + t.Run("load playlist json", func(t *testing.T) { + obj, gvk, err := DecodeYAMLObject(bytes.NewReader([]byte(`{ + "kind": "Playlist", + "apiVersion": "playlist.grafana.app/v0alpha1", + "metadata": { + "name": "hello" + }, + "spec": { + "title": "Playlist from provisioning" + } + }`))) + + require.NoError(t, err) + require.Equal(t, &schema.GroupVersionKind{ + Group: "playlist.grafana.app", + Version: "v0alpha1", + Kind: "Playlist", + }, gvk) + require.NotNil(t, obj) + }) + + t.Run("YAML Parsing", func(t *testing.T) { + obj, gvk, err := DecodeYAMLObject(bytes.NewReader([]byte("kind: xyz\npi: 3.1415"))) + require.NoError(t, err) + require.NotNil(t, gvk) + require.Equal(t, "xyz", obj.Object["kind"]) + require.Equal(t, 3.1415, obj.Object["pi"]) + + // // Tabs in the value + // _, _, err = LoadYAMLOrJSON(bytes.NewReader([]byte("kind: xyz\n\tpi: 3.1415"))) + // require.Equal(t, ErrYamlContainsTabs, err) + }) + + t.Run("load playlist yaml", func(t *testing.T) { + obj, gvk, err := DecodeYAMLObject(bytes.NewReader([]byte(` +apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +metadata: + name: hello +spec: + title: a title +`))) + + require.NoError(t, err) + require.Equal(t, &schema.GroupVersionKind{ + Group: "playlist.grafana.app", + Version: "v0alpha1", + Kind: "Playlist", + }, gvk) + require.NotNil(t, obj) + }) + + t.Run("load dashboard json", func(t *testing.T) { + // Support dashboard conversion + obj, gvk, classic, err := ReadClassicResource(context.Background(), &repository.FileInfo{ + Data: []byte(`{ + "schemaVersion": 7, + "panels": [], + "tags": [] + }`), + }) + + require.NoError(t, err) + require.Equal(t, provisioning.ClassicDashboard, classic) + require.Equal(t, &schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Kind: "Dashboard", + }, gvk) + require.NotNil(t, obj) + }) + + t.Run("lint dashboard", func(t *testing.T) { + var err error + info := &repository.FileInfo{ + Path: "devenv/dev-dashboards/panel-timeline/timeline-demo.json", + } + info.Data, err = os.ReadFile(path.Join("../../../../..", info.Path)) + require.NoError(t, err) + + parser := &Parser{ + repo: provisioning.ResourceRepositoryInfo{ + Name: "test", + }, + } + + // try to validate (and lint) + validate := true + + // Support dashboard conversion + parsed, err := parser.Parse(context.Background(), info, validate) + require.Error(t, err) // no clients configured! + + require.Equal(t, provisioning.ClassicDashboard, parsed.Classic) + require.Equal(t, &schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Kind: "Dashboard", + }, parsed.GVK) + }) +} diff --git a/pkg/registry/apis/provisioning/resources/filepath.go b/pkg/registry/apis/provisioning/resources/filepath.go new file mode 100644 index 00000000000..ca592904363 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/filepath.go @@ -0,0 +1,42 @@ +package resources + +import ( + "errors" + "path" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +var ( + ErrPathTooDeep = errors.New("the path is too deep") + ErrUnsupportedFileExtension = errors.New("unsupported file extension") + ErrNotRelative = errors.New("path must be relative to the root") +) + +const maxPathDepth = 8 + +// IsPathSupported checks if the file path is supported by the provisioning API. +// it also validates if the path is safe and if the file extension is supported. +func IsPathSupported(filePath string) error { + // Validate the path for any traversal attempts first + if err := safepath.IsSafe(filePath); err != nil { + return err + } + + if safepath.Depth(filePath) > maxPathDepth { + return ErrPathTooDeep + } + + if safepath.IsAbs(filePath) { + return ErrNotRelative + } + + // Only check file extension if it's not a folder path + if !safepath.IsDir(filePath) { + if ext := path.Ext(filePath); ext != ".yml" && ext != ".yaml" && ext != ".json" { + return ErrUnsupportedFileExtension + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/resources/filepath_test.go b/pkg/registry/apis/provisioning/resources/filepath_test.go new file mode 100644 index 00000000000..89cf98180fa --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/filepath_test.go @@ -0,0 +1,72 @@ +package resources + +import ( + "testing" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/stretchr/testify/require" +) + +func TestIsPathSupported(t *testing.T) { + tests := []struct { + name string + path string + expectedErr error + }{ + { + name: "valid yaml file", + path: "dashboards/my-dashboard.yaml", + }, + { + name: "valid yml file", + path: "dashboards/my-dashboard.yml", + }, + { + name: "valid json file", + path: "dashboards/my-dashboard.json", + }, + { + name: "valid nested path", + path: "dashboards/folder1/folder2/my-dashboard.yaml", + }, + { + name: "valid directory path", + path: "dashboards/folder1/", + }, + { + name: "unsupported file extension", + path: "dashboards/my-dashboard.txt", + expectedErr: ErrUnsupportedFileExtension, + }, + { + name: "path traversal attempt", + path: "../dashboards/my-dashboard.yaml", + expectedErr: safepath.ErrPathTraversalAttempt, + }, + { + name: "path too deep", + path: "level1/level2/level3/level4/level5/level6/level7/level8/level9/dashboard.yaml", + expectedErr: ErrPathTooDeep, + }, + { + name: "absolute path", + path: "/etc/dashboards/my-dashboard.yaml", + expectedErr: ErrNotRelative, + }, + { + name: "empty directory path", + path: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := IsPathSupported(tt.path) + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/resources/folders.go b/pkg/registry/apis/provisioning/resources/folders.go new file mode 100644 index 00000000000..c301d676720 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/folders.go @@ -0,0 +1,133 @@ +package resources + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +type FolderManager struct { + repo repository.Repository + lookup *FolderTree + client dynamic.ResourceInterface +} + +func NewFolderManager(repo repository.Repository, client dynamic.ResourceInterface) *FolderManager { + return &FolderManager{ + repo: repo, + lookup: NewEmptyFolderTree(), + client: client, + } +} + +func (fm *FolderManager) Client() dynamic.ResourceInterface { + return fm.client +} + +// EnsureFoldersExist creates the folder structure in the cluster. +func (fm *FolderManager) EnsureFolderPathExist(ctx context.Context, filePath string) (parent string, err error) { + cfg := fm.repo.Config() + parent = RootFolder(cfg) + + dir := filePath + if !safepath.IsDir(filePath) { + dir = safepath.Dir(filePath) + } + + if dir == "" { + return parent, nil + } + + f := ParseFolder(dir, cfg.Name) + if fm.lookup.In(f.ID) { + return f.ID, nil + } + + err = safepath.Walk(ctx, f.Path, func(ctx context.Context, traverse string) error { + f := ParseFolder(traverse, cfg.GetName()) + if fm.lookup.In(f.ID) { + parent = f.ID + return nil + } + + if err := fm.EnsureFolderExists(ctx, f, parent); err != nil { + return fmt.Errorf("ensure folder exists: %w", err) + } + + fm.lookup.Add(f, parent) + parent = f.ID + return nil + }) + + if err != nil { + return "", err + } + + return f.ID, nil +} + +// EnsureFolderExists creates the folder if it doesn't exist. +// If the folder already exists: +// - it will error if the folder is not owned by this repository +func (fm *FolderManager) EnsureFolderExists(ctx context.Context, folder Folder, parent string) error { + cfg := fm.repo.Config() + obj, err := fm.client.Get(ctx, folder.ID, metav1.GetOptions{}) + if err == nil { + current, ok := obj.GetAnnotations()[utils.AnnoKeyManagerIdentity] + if !ok { + return fmt.Errorf("target folder is not managed by a repository") + } + if current != cfg.Name { + return fmt.Errorf("target folder is managed by a different repository (%s)", current) + } + return nil + } else if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to check if folder exists: %w", err) + } + + obj = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]any{ + "title": folder.Title, + }, + }, + } + obj.SetAPIVersion(v0alpha1.APIVERSION) + obj.SetKind(v0alpha1.FolderResourceInfo.GroupVersionKind().Kind) + obj.SetNamespace(cfg.GetNamespace()) + obj.SetName(folder.ID) + + meta, err := utils.MetaAccessor(obj) + if err != nil { + return fmt.Errorf("create meta accessor for the object: %w", err) + } + + if parent != "" { + meta.SetFolder(parent) + } + meta.SetManagerProperties(utils.ManagerProperties{ + Kind: utils.ManagerKindRepo, + Identity: cfg.GetName(), + }) + meta.SetSourceProperties(utils.SourceProperties{ + Path: folder.Path, + }) + + if _, err := fm.client.Create(ctx, obj, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("failed to create folder: %w", err) + } + return nil +} + +func (fm *FolderManager) GetFolder(ctx context.Context, name string) (*unstructured.Unstructured, error) { + return fm.client.Get(ctx, name, metav1.GetOptions{}) +} diff --git a/pkg/registry/apis/provisioning/resources/id.go b/pkg/registry/apis/provisioning/resources/id.go new file mode 100644 index 00000000000..b9d14734c10 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/id.go @@ -0,0 +1,141 @@ +package resources + +import ( + "crypto/sha256" + "encoding/base64" + "strings" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +// sanitiseKubeName removes all characters that don't fulfil the DNS subdomain name rules: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names +// > contain no more than 253 characters +// > contain only lowercase alphanumeric characters, '-' or '.' +// > start with an alphanumeric character +// > end with an alphanumeric character +// +// That said, the Kubernetes name is extended with one more rule: it must not contain more than 40 characters. +// We do this to support storage modes that write to legacy storage, where UIDs are limited to 40 characters or less. +// +// If no characters are valid, this returns an empty string. +func sanitiseKubeName(s string) string { + // Note: Builder never returns an error. + var b strings.Builder + lastHyphen := false // Having at most 1 hyphen in a row is not a requirement, but is closer to standard convention. + for _, r := range strings.ToLower(s) { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '-' { + // We don't want to have multiple hyphens following one another. + if !lastHyphen || r != '-' { + lastHyphen = r == '-' + _, _ = b.WriteRune(r) + } + } else if r >= 'A' && r <= 'Z' { + lastHyphen = false + r += 'a' - 'A' + _, _ = b.WriteRune(r) + } else if (r == '_' || r == '/') && !lastHyphen { // Special-case for common characters that we still want to keep somehow + lastHyphen = true + _, _ = b.WriteRune('-') + } + // Else, skip it, silently. + + if b.Len() == 40 { + // Technically, we could be less than 40 after some more cleaning... but this is good enough. + break + } + } + str := b.String() + // We must start and end with alphanumerics. + str = strings.Trim(str, "-") + str = strings.Trim(str, ".") + + return str +} + +// appendHashSuffix creates a function that modifies an input string to include a hash suffix. +// The goal of this function is to represent all needs for hashing IDs. +// +// The hash uses the input hashKey and repositoryName as a salt. +// The output string is at most 40 characters long. (253 is Kubernetes' limit, but UIDs have a max of 40 characters.) +// The output string is a valid Kubernetes name (see [sanitiseKubeName]). +// The output string contains at least 12 characters of a hash, plus a 1 character hyphen to separate the input and the hash. +// The function is deterministic given the same invocation parameters to this function. +func appendHashSuffix(hashKey, repositoryName string) func(string) string { + salt := []byte(repositoryName + "/" + hashKey) + + const maxLen = 40 // valid Kubernetes name + const minSuffix = 8 // excluding hyphen + const minSpace = minSuffix + 1 // +1 for hyphen + + return func(s string) string { + hasher := sha256.New() + // From hash.Hash docs: + // > Write (via the embedded io.Writer interface) adds more data to the running hash. + // > It never returns an error. + // As such, we ignore all errors. + _, _ = hasher.Write(salt) // Input to the parent function + _, _ = hasher.Write([]byte(s)) + hash := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + hash = sanitiseKubeName(hash) // We have rules to follow, as per our doc contract + + if len(s) > maxLen-minSpace { + s = s[:maxLen-minSpace] + } + + spaceForHash := maxLen - len(s) - 1 + if spaceForHash < len(hash) { + hash = hash[:spaceForHash] + } + return sanitiseKubeName(s + "-" + hash) + } +} + +// Will pick a name based on the hashed repository and path +func NamesFromHashedRepoPath(repo string, fpath string) (string, string) { + // Remove the extension: we don't want the extension to impact the ID. This lets the user change between all supported formats. + fpath = safepath.RemoveExt(fpath) + hasher := appendHashSuffix(fpath, repo) + + return hasher(safepath.Base(fpath)), hasher(strings.Trim(safepath.Dir(fpath), "/")) +} + +// Folder contains the data for a folder we use in provisioning. +type Folder struct { + // Title is the human-readable name created by a human who wrote it. + Title string + // ID represents the name the folder should have, derived from the title. + // It contains a suffix calculated from the path of the folder. + // The ID is used in Kubernetes and the folders API server. This is the same as the legacy (and by the time you read this, hopefully removed) UID concept of folders. + ID string + // Path is the full path to the folder, as given to the parse function. + Path string +} + +func ParseFolder(dirPath, repositoryName string) Folder { + hasher := appendHashSuffix(strings.TrimSuffix(dirPath, "/"), repositoryName) + base := safepath.Base(dirPath) + + return Folder{ + Title: base, + ID: hasher(sanitiseKubeName(base)), + Path: dirPath, + } +} + +func RootFolder(repository *provisioning.Repository) string { + if repository.Spec.Sync.Target == provisioning.SyncTargetTypeFolder { + return repository.Name // a folder with the same identifier as the repository + } + + return "" +} + +func ParentFolder(filePath string, repository *provisioning.Repository) string { + parent := safepath.Dir(filePath) + if parent == "" { + return RootFolder(repository) + } + + return ParseFolder(parent, repository.GetName()).ID +} diff --git a/pkg/registry/apis/provisioning/resources/id_test.go b/pkg/registry/apis/provisioning/resources/id_test.go new file mode 100644 index 00000000000..77c319b72a1 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/id_test.go @@ -0,0 +1,109 @@ +package resources + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSanitiseKubeName(t *testing.T) { + for _, tc := range []struct{ Name, Input, Expected string }{ + {"Valid Kubernetes name", "a-b.123.-c", "a-b.123.-c"}, + {"Capital letters lowercase", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"}, + {"Trailing hyphen removed", "abc-", "abc"}, + {"Trailing dot removed", "abc.", "abc"}, + {"Leading hyphen removed", "-abc", "abc"}, + {"Leading dot removed", ".abc", "abc"}, + {"Double hyphen simplified", "ab--c", "ab-c"}, + {"Five hyphens simplified", "ab-----c", "ab-c"}, + {"Underscore converted", "ab_c", "ab-c"}, + {"Slash converted", "ab/c", "ab-c"}, + {"Stops at 40 characters", strings.Repeat("a", 300), strings.Repeat("a", 40)}, + } { + t.Run(tc.Name, func(t *testing.T) { + assert.Equal(t, tc.Expected, sanitiseKubeName(tc.Input)) + }) + } +} + +func TestAppendHashSuffix(t *testing.T) { + t.Run("hash key is part of hash", func(t *testing.T) { + keyA := appendHashSuffix("A", "unit-test")("test") + keyB := appendHashSuffix("B", "unit-test")("test") + assert.NotEqual(t, keyA, keyB, "hash key is not part of hash") + }) + t.Run("repository name is part of hash", func(t *testing.T) { + repoA := appendHashSuffix("unit-test", "A")("test") + repoB := appendHashSuffix("unit-test", "B")("test") + assert.NotEqual(t, repoA, repoB, "repo name is not part of hash") + }) + t.Run("is deterministic", func(t *testing.T) { + hasher := appendHashSuffix("unit", "test") + assert.Equal(t, hasher("a"), hasher("a")) + _ = hasher("b") // Assume it isn't deterministic: this would likely modify a hasher state + assert.Equal(t, hasher("a"), hasher("a")) // alas, it is deterministic! + }) + + // These are covered by the tests above, so we can just use static values from now on + const repoName = "repository" + const hashKey = "key" + hasher := appendHashSuffix(hashKey, repoName) + + for _, tc := range []struct{ Name, Input, Expected string }{ + {"Simple, short prefix", "test", "test-ae2h65vh3ygoxmprmnludpyhhpr3d-5iosy"}, + {"Suffix requiring cutting hash to min", strings.Repeat("test", 200), "testtesttesttesttesttesttesttes-8dogmh7b"}, + {"Suffix requiring partially cutting hash", strings.Repeat("test", 7), "testtesttesttesttesttesttest-icauzj-i5j5"}, + } { + t.Run(tc.Name, func(t *testing.T) { + hashed := hasher(tc.Input) + assert.Equal(t, tc.Expected, hashed, "hashed value must be as expected") + + // These exist both because they're helpful to understand how something isn't equal above, and for programmer errors. (e.g. what if I manually input a 41 char expected value? or one with too few hash chars?) + // We only want 40 characters because UIDs support no more. When we get rid of legacy storage, we can extend the support to 253 character long strings. + assert.LessOrEqual(t, len(hashed), 40, "string after hashing needs to be <=40 chars long") + assert.GreaterOrEqual(t, len(strings.SplitAfterN(hashed, "-", 2)[1]), 8, "hash must be at least 8 characters long") + }) + } +} + +func TestNamesFromHashedRepoPath(t *testing.T) { + dashName, folderName := NamesFromHashedRepoPath("xyz", "path/to/folder/dashboard.json") + assert.Equal(t, "dashboard-fy2kflbmskvt6u-9uecoahd1ekwbb7", dashName, "dashboard name of dashboard.json") + assert.Equal(t, "path-to-folder-3ehfurpmbvs4yxfp7a0r2uskr", folderName, "folder name of dashboard.json") + // We only want 40 characters because UIDs support no more. When we get rid of legacy storage, we can extend the support to 253 character long strings. + assert.LessOrEqual(t, len(dashName), 40, "dashName after hashing needs to be <=40 chars long") + assert.LessOrEqual(t, len(folderName), 40, "folderName after hashing needs to be <=40 chars long") + + name1, f1 := NamesFromHashedRepoPath("xyz", "path/to/folder.json") + name2, f2 := NamesFromHashedRepoPath("xyz", "path/to/folder") + assert.Equal(t, name1, name2, "folder.json should have same object name as folder (no extension)") + assert.Equal(t, f1, f2, "folder.json and folder should have same folder name as each other") +} + +func TestParseFolderID(t *testing.T) { + const repoName = "unit-test" // we have other tests verifying the repo name changes the id + + cases := []struct { + Description string + Path string + Title string + KubeName string + }{ + {"Short, simple path", "hello/world", "world", "world-wik-hjayboohlsvzzr2ob3he8cs7ffk0jd"}, + {"Capital letters and punctuation", "Hello, World!", "Hello, World!", "helloworld-sbcnvdmezf0jnvgfhpk5ewaoawbeg"}, + {"Very long name", strings.Repeat("/hello/world", 200), "world", "world-bc9jpbg6ctg-w-pexkul-f1ic-bwer5-3r"}, + } + + for _, c := range cases { + t.Run(c.Description, func(t *testing.T) { + id := ParseFolder(c.Path, repoName) + assert.Equal(t, c.Path, id.Path) + assert.Equal(t, c.KubeName, id.ID) + assert.Equal(t, c.Title, id.Title) + + // We only want 40 characters because UIDs support no more. When we get rid of legacy storage, we can extend the support to 253 character long strings. + assert.LessOrEqual(t, len(id.ID), 40, "ID after hashing needs to be <=40 chars long") + }) + } +} diff --git a/pkg/registry/apis/provisioning/resources/object.go b/pkg/registry/apis/provisioning/resources/object.go new file mode 100644 index 00000000000..c5702dfe22e --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/object.go @@ -0,0 +1,165 @@ +package resources + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +// Get repository stats +type ResourceLister interface { + List(ctx context.Context, namespace, repository string) (*provisioning.ResourceList, error) + Stats(ctx context.Context, namespace, repository string) (*provisioning.ResourceStats, error) +} + +type ResourceListerFromSearch struct { + managed resource.ManagedObjectIndexClient + index resource.ResourceIndexClient + legacyMigrator legacy.LegacyMigrator + storageStatus dualwrite.Service +} + +func NewResourceLister( + managed resource.ManagedObjectIndexClient, + index resource.ResourceIndexClient, + legacyMigrator legacy.LegacyMigrator, + storageStatus dualwrite.Service, +) ResourceLister { + return &ResourceListerFromSearch{ + index: index, + managed: managed, + legacyMigrator: legacyMigrator, + storageStatus: storageStatus, + } +} + +// List implements ResourceLister. +func (o *ResourceListerFromSearch) List(ctx context.Context, namespace, repository string) (*provisioning.ResourceList, error) { + objects, err := o.managed.ListManagedObjects(ctx, &resource.ListManagedObjectsRequest{ + Namespace: namespace, + Kind: string(utils.ManagerKindRepo), + Id: repository, + }) + if err != nil { + return nil, err + } + if objects.Error != nil { + return nil, resource.GetError(objects.Error) + } + + list := &provisioning.ResourceList{} + for _, v := range objects.Items { + list.Items = append(list.Items, provisioning.ResourceListItem{ + Path: v.Path, + Group: v.Object.Group, + Resource: v.Object.Resource, + Name: v.Object.Name, + Hash: v.Hash, + Time: v.Time, + Title: v.Title, + Folder: v.Folder, + }) + } + return list, nil +} + +// Stats implements ResourceLister. +func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, repository string) (*provisioning.ResourceStats, error) { + req := &resource.CountManagedObjectsRequest{ + Namespace: namespace, + } + if repository != "" { + req.Kind = string(utils.ManagerKindRepo) + req.Id = repository + } + + counts, err := o.managed.CountManagedObjects(ctx, req) + if err != nil { + return nil, err + } + if counts.Error != nil { + return nil, resource.GetError(counts.Error) + } + + lookup := make(map[string]*provisioning.ManagerStats) + for _, v := range counts.Items { + key := v.Kind + ":" + v.Id + m := lookup[key] + if m == nil { + m = &provisioning.ManagerStats{ + Kind: utils.ManagerKind(v.Kind), + Identity: v.Id, + } + lookup[key] = m + } + m.Stats = append(m.Stats, provisioning.ResourceCount{ + Group: v.Group, + Resource: v.Resource, + Count: v.Count, + }) + } + stats := &provisioning.ResourceStats{ + TypeMeta: metav1.TypeMeta{ + APIVersion: provisioning.SchemeGroupVersion.String(), + Kind: "ResourceStats", + }, + } + for _, v := range lookup { + stats.Managed = append(stats.Managed, *v) + } + + // When selecting an explicit repository, do not fetch global stats + if repository != "" { + return stats, nil + } + + // Get the stats based on what a migration could support + if dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { + rsp, err := o.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + Namespace: namespace, + Resources: []schema.GroupResource{{ + Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, + }, { + Group: folders.GROUP, Resource: folders.RESOURCE, + }}, + WithHistory: false, + OnlyCount: true, + }) + if err != nil { + return nil, err + } + for _, v := range rsp.Summary { + stats.Instance = append(stats.Instance, provisioning.ResourceCount{ + Group: v.Group, + Resource: v.Resource, + Count: v.Count, + }) + } + return stats, nil + } + + // Get full instance stats + info, err := o.index.GetStats(ctx, &resource.ResourceStatsRequest{ + Namespace: namespace, + }) + if err != nil { + return nil, err + } + for _, v := range info.Stats { + stats.Instance = append(stats.Instance, provisioning.ResourceCount{ + Group: v.Group, + Resource: v.Resource, + Count: v.Count, + }) + } + return stats, nil +} diff --git a/pkg/registry/apis/provisioning/resources/parser.go b/pkg/registry/apis/provisioning/resources/parser.go new file mode 100644 index 00000000000..a4a3f581025 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/parser.go @@ -0,0 +1,294 @@ +package resources + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "path" + + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +var ( + ErrNamespaceMismatch = errors.New("the file namespace does not match target namespace") +) + +type ParserFactory struct { + ClientFactory *ClientFactory +} + +func (f *ParserFactory) GetParser(ctx context.Context, repo repository.Reader) (*Parser, error) { + config := repo.Config() + + clients, err := f.ClientFactory.Clients(ctx, config.GetNamespace()) + if err != nil { + return nil, err + } + + urls, _ := repo.(repository.RepositoryWithURLs) + return &Parser{ + repo: provisioning.ResourceRepositoryInfo{ + Type: config.Spec.Type, + Title: config.Spec.Title, + Namespace: config.Namespace, + Name: config.Name, + }, + urls: urls, + clients: clients, + }, nil +} + +type Parser struct { + // The target repository + repo provisioning.ResourceRepositoryInfo + + // for repositories that have URL support + urls repository.RepositoryWithURLs + + // ResourceClients give access to k8s apis + clients *ResourceClients +} + +type ParsedResource struct { + // Original file Info + Info *repository.FileInfo + + // The repository details + Repo provisioning.ResourceRepositoryInfo + + // Resource URLs + URLs *provisioning.ResourceURLs + + // Check for classic file types (dashboard.json, etc) + Classic provisioning.ClassicFileType + + // Parsed contents + Obj *unstructured.Unstructured + // Metadata accessor for the file object + Meta utils.GrafanaMetaAccessor + + // The Kind is defined in the file + GVK *schema.GroupVersionKind + // The Resource is found by mapping Kind to the right apiserver + GVR *schema.GroupVersionResource + // Client that can talk to this resource + Client dynamic.ResourceInterface + + // The Existing object (same name) + // ?? do we need/want the whole thing?? + Existing *unstructured.Unstructured + + // Create or Update + Action provisioning.ResourceAction + + // The results from dry run + DryRunResponse *unstructured.Unstructured + + // When the value has been saved in the grafana database + Upsert *unstructured.Unstructured + + // If we got some Errors + Errors []error +} + +func (r *Parser) Clients() *ResourceClients { + return r.clients +} + +func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate bool) (parsed *ParsedResource, err error) { + logger := logging.FromContext(ctx).With("path", info.Path, "validate", validate) + parsed = &ParsedResource{ + Info: info, + Repo: r.repo, + } + + if err := IsPathSupported(info.Path); err != nil { + return parsed, err + } + + if info.Path == "" { + return parsed, errors.New("path is required") + } + + parsed.Obj, parsed.GVK, err = DecodeYAMLObject(bytes.NewBuffer(info.Data)) + if err != nil { + logger.Debug("failed to find GVK of the input data", "error", err) + parsed.Obj, parsed.GVK, parsed.Classic, err = ReadClassicResource(ctx, info) + if err != nil { + logger.Debug("also failed to get GVK from fallback loader?", "error", err) + return parsed, err + } + } + + if r.urls != nil { + parsed.URLs, err = r.urls.ResourceURLs(ctx, info) + if err != nil { + logger.Debug("failed to load resource URLs", "error", err) + return parsed, err + } + } + + // Remove the internal dashboard UID,version and id if they exist + if parsed.GVK.Group == dashboard.GROUP && parsed.GVK.Kind == "Dashboard" { + unstructured.RemoveNestedField(parsed.Obj.Object, "spec", "uid") + unstructured.RemoveNestedField(parsed.Obj.Object, "spec", "version") + unstructured.RemoveNestedField(parsed.Obj.Object, "spec", "id") // now managed as a label + } + + parsed.Meta, err = utils.MetaAccessor(parsed.Obj) + if err != nil { + return nil, err + } + obj := parsed.Obj + + // Validate the namespace + if obj.GetNamespace() != "" && obj.GetNamespace() != r.repo.Namespace { + parsed.Errors = append(parsed.Errors, ErrNamespaceMismatch) + } + + obj.SetNamespace(r.repo.Namespace) + parsed.Meta.SetManagerProperties(utils.ManagerProperties{ + Kind: utils.ManagerKindRepo, + Identity: r.repo.Name, + }) + parsed.Meta.SetSourceProperties(utils.SourceProperties{ + Path: info.Path, // joinPathWithRef(info.Path, info.Ref), + Checksum: info.Hash, + }) + + // Calculate name+folder from the file path + if info.Path != "" { + objName, folderName := NamesFromHashedRepoPath(r.repo.Name, info.Path) + parsed.Meta.SetFolder(folderName) + if obj.GetName() == "" { + obj.SetName(objName) // use the name saved in config + } + } + obj.SetUID("") // clear identifiers + obj.SetResourceVersion("") // clear identifiers + + // We can not do anything more if no kind is defined + if parsed.GVK == nil { + return parsed, nil + } + + if r.clients == nil { + return parsed, fmt.Errorf("no client configured") + } + + client, gvr, err := r.clients.ForKind(*parsed.GVK) + if err != nil { + return nil, err // does not map to a resour e + } + + parsed.GVR = &gvr + parsed.Client = client + if !validate { + return parsed, nil + } + + if parsed.Client == nil { + parsed.Errors = append(parsed.Errors, fmt.Errorf("unable to find client")) + return parsed, nil + } + + // Dry run CREATE or UPDATE + parsed.Existing, _ = parsed.Client.Get(ctx, obj.GetName(), metav1.GetOptions{}) + if parsed.Existing == nil { + parsed.Action = provisioning.ResourceActionCreate + parsed.DryRunResponse, err = parsed.Client.Create(ctx, obj, metav1.CreateOptions{ + DryRun: []string{"All"}, + }) + } else { + parsed.Action = provisioning.ResourceActionUpdate + parsed.DryRunResponse, err = parsed.Client.Update(ctx, obj, metav1.UpdateOptions{ + DryRun: []string{"All"}, + }) + } + if err != nil { + parsed.Errors = append(parsed.Errors, err) + } + return parsed, nil +} + +func (f *ParsedResource) ToSaveBytes() ([]byte, error) { + // TODO? should we use the dryRun (validated) version? + obj := make(map[string]any) + for k, v := range f.Obj.Object { + if k != "metadata" { + obj[k] = v + } + } + + switch path.Ext(f.Info.Path) { + // JSON pretty print + case ".json": + return json.MarshalIndent(obj, "", " ") + + // Write the value as yaml + case ".yaml", ".yml": + return yaml.Marshal(obj) + + default: + return nil, fmt.Errorf("unexpected format") + } +} + +func (f *ParsedResource) AsResourceWrapper() *provisioning.ResourceWrapper { + info := f.Info + res := provisioning.ResourceObjects{ + Type: provisioning.ResourceType{ + Classic: f.Classic, + }, + Action: f.Action, + } + + if f.GVK != nil { + res.Type.Group = f.GVK.Group + res.Type.Version = f.GVK.Version + res.Type.Kind = f.GVK.Kind + } + + // The resource (GVR) is derived from the kind (GVK) + if f.GVR != nil { + res.Type.Resource = f.GVR.Resource + } + + if f.Obj != nil { + res.File = v0alpha1.Unstructured{Object: f.Obj.Object} + } + if f.Existing != nil { + res.Existing = v0alpha1.Unstructured{Object: f.Existing.Object} + } + if f.Upsert != nil { + res.Upsert = v0alpha1.Unstructured{Object: f.Upsert.Object} + } else if f.DryRunResponse != nil { + res.DryRun = v0alpha1.Unstructured{Object: f.DryRunResponse.Object} + } + wrap := &provisioning.ResourceWrapper{ + Path: info.Path, + Ref: info.Ref, + Hash: info.Hash, + Repository: f.Repo, + URLs: f.URLs, + Timestamp: info.Modified, + Resource: res, + } + for _, err := range f.Errors { + wrap.Errors = append(wrap.Errors, err.Error()) + } + return wrap +} diff --git a/pkg/registry/apis/provisioning/resources/tree.go b/pkg/registry/apis/provisioning/resources/tree.go new file mode 100644 index 00000000000..b11ac5a3cb7 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/tree.go @@ -0,0 +1,139 @@ +package resources + +import ( + "context" + "fmt" + "sort" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +// FolderTree contains the entire set of folders (at a given snapshot in time) of the Grafana instance. +// The folders are portrayed as a tree, where a folder has a parent, up until the root folder. +// The root folder is special-cased as a folder that exists, but is not itself stored. It has no ID, no title, and no data, but will return `true` for OK bools. +type FolderTree struct { + tree map[string]string + folders map[string]Folder +} + +// In determines if the given folder is in the tree at all. That is, it answers "does the folder even exist in the Grafana instance?" +// An empty folder string means the root folder, and is special-cased to always return true. +func (t *FolderTree) In(folder string) bool { + _, ok := t.tree[folder] + return ok || folder == "" +} + +// DirPath creates the path to the directory with slashes, up to but not including the baseFolder. +// The baseFolder is expected to be the repository's root folder, as defined by its spec. If this is used in other contexts, it should still function. +// An empty folder string means the root folder, and is special-cased to return no ID data. +// The returned Path field in the ID is relative to the base folder. If it is the base folder, an empty string is returned. +// +// If In(folder) or In(baseFolder) is false, this will return ok=false, because it would be undefined behaviour. +// If baseFolder is not a parent of folder, ok=false is returned. +func (t *FolderTree) DirPath(folder, baseFolder string) (fid Folder, ok bool) { + if !t.In(folder) || !t.In(baseFolder) { + return Folder{}, false + } + if folder == "" && baseFolder != "" { + return Folder{}, false + } else if folder == baseFolder { + // Zero-value: we're fine with the zv if we're working with the root folder here. + // Any other folder ID will have the correct metadata and no path (which is correct). + return t.folders[folder], true + } + + fid = t.folders[folder] + fid.Path = fid.Title + ok = baseFolder == "" + + parent := t.tree[folder] + for parent != "" { + if parent == baseFolder { + ok = true + break + } + fid.Path = safepath.Join(t.folders[parent].Title, fid.Path) + parent = t.tree[parent] + } + return fid, ok +} + +func (t *FolderTree) Add(folder Folder, parent string) { + t.tree[folder.ID] = parent + t.folders[folder.ID] = folder +} + +type WalkFunc func(ctx context.Context, folder Folder) error + +func (t *FolderTree) Walk(ctx context.Context, fn WalkFunc) error { + toWalk := make([]Folder, 0, len(t.folders)) + for _, folder := range t.folders { + folder, _ := t.DirPath(folder.ID, "") + toWalk = append(toWalk, folder) + } + + // sort by depth of the paths + sort.Slice(toWalk, func(i, j int) bool { + return safepath.Depth(toWalk[i].Path) < safepath.Depth(toWalk[j].Path) + }) + + for _, folder := range toWalk { + if err := fn(ctx, folder); err != nil { + return err + } + } + + return nil +} + +func NewEmptyFolderTree() *FolderTree { + return &FolderTree{ + tree: make(map[string]string, 0), + folders: make(map[string]Folder, 0), + } +} + +func (t *FolderTree) AddUnstructured(item *unstructured.Unstructured, skipRepo string) error { + meta, err := utils.MetaAccessor(item) + if err != nil { + return fmt.Errorf("extract meta accessor: %w", err) + } + manager, _ := meta.GetManagerProperties() + if manager.Identity == skipRepo { + return nil // skip it... already in tree? + } + folder := Folder{ + Title: meta.FindTitle(item.GetName()), + ID: item.GetName(), + } + t.tree[folder.ID] = meta.GetFolder() + t.folders[folder.ID] = folder + return nil +} + +func NewFolderTreeFromResourceList(resources *provisioning.ResourceList) *FolderTree { + tree := make(map[string]string, len(resources.Items)) + folderIDs := make(map[string]Folder, len(resources.Items)) + for _, rf := range resources.Items { + if rf.Group != folders.GROUP { + continue + } + + tree[rf.Name] = rf.Folder + folderIDs[rf.Name] = Folder{ + Title: rf.Title, + ID: rf.Name, + Path: rf.Path, + } + } + + return &FolderTree{ + tree, + folderIDs, + } +} diff --git a/pkg/registry/apis/provisioning/resources/tree_test.go b/pkg/registry/apis/provisioning/resources/tree_test.go new file mode 100644 index 00000000000..e19a0980b3e --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/tree_test.go @@ -0,0 +1,92 @@ +package resources + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFolderTree(t *testing.T) { + newFid := func(kube, title string) Folder { + return Folder{ID: kube, Title: title} + } + + t.Run("empty tree", func(t *testing.T) { + tree := &FolderTree{ + tree: make(map[string]string), + folders: make(map[string]Folder), + } + + assert.False(t, tree.In("x"), "x should not be in tree") + assert.False(t, tree.In("z"), "z should not be in tree") + _, ok := tree.DirPath("x", "") + assert.False(t, ok, "x should not have a DirPath") + }) + + t.Run("single directory in tree", func(t *testing.T) { + tree := &FolderTree{ + tree: map[string]string{"x": ""}, + folders: map[string]Folder{"x": newFid("x", "X!")}, + } + + assert.True(t, tree.In("x"), "x should be in tree") + id, ok := tree.DirPath("x", "x") + if assert.True(t, ok, "x should have DirPath with itself as base") { + assert.Equal(t, "x", id.ID, "KubernetesName") + assert.Equal(t, "X!", id.Title, "Title") + assert.Equal(t, "", id.Path, "Path") + } + id, ok = tree.DirPath("x", "") + if assert.True(t, ok, "x should have DirPath with empty base") { + assert.Equal(t, "x", id.ID, "KubernetesName") + assert.Equal(t, "X!", id.Title, "Title") + assert.Equal(t, "X!", id.Path, "Path") + } + }) + + t.Run("simple nesting tree", func(t *testing.T) { + tree := &FolderTree{ + tree: map[string]string{"a": "b", "b": "c", "c": "x", "x": ""}, + folders: map[string]Folder{ + "x": newFid("x", "X!"), + "c": newFid("c", "C :)"), + "b": newFid("b", "!!B#!"), + "a": newFid("a", "[€]@£a"), + }, + } + + assert.True(t, tree.In("x"), "x should be in tree") + assert.True(t, tree.In("a"), "a should be in tree") + assert.False(t, tree.In("z"), "z should not be in tree, for it is undeclared") + + id, ok := tree.DirPath("x", "") + if assert.True(t, ok, "x should have DirPath with empty base") { + assert.Equal(t, "x", id.ID, "KubernetesName") + assert.Equal(t, "X!", id.Title, "Title") + assert.Equal(t, "X!", id.Path, "Path") + } + + id, ok = tree.DirPath("c", "c") + if assert.True(t, ok, "c should have DirPath with itself as base") { + assert.Equal(t, "c", id.ID, "KubernetesName") + assert.Equal(t, "C :)", id.Title, "Title") + assert.Equal(t, "", id.Path, "Path") + } + + id, ok = tree.DirPath("a", "x") + if assert.True(t, ok, "a should have DirPath with x as base") { + assert.Equal(t, "a", id.ID, "KubernetesName") + assert.Equal(t, "[€]@£a", id.Title, "Title") + assert.Equal(t, "C :)/!!B#!/[€]@£a", id.Path, "Path") + } + _, ok = tree.DirPath("x", "a") + assert.False(t, ok, "x should not have DirPath with a as base, because a is a subfolder of x") + + id, ok = tree.DirPath("", "") + if assert.True(t, ok, "the root folder should have a path to itself") { + assert.Empty(t, id.ID) + assert.Empty(t, id.Path) + assert.Empty(t, id.Title) + } + }) +} diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go new file mode 100644 index 00000000000..6d9f89db34d --- /dev/null +++ b/pkg/registry/apis/provisioning/routes.go @@ -0,0 +1,174 @@ +package provisioning + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + authlib "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/util/errhttp" +) + +// TODO: Move the specific logic to the connector so that we don't have logic all over the place. +// GetAPIRoutes implements the direct HTTP handlers that bypass k8s +func (b *APIBuilder) GetAPIRoutes() *builder.APIRoutes { + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "stats", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + OperationId: "getResourceStats", // used for RTK client + Tags: []string{"Provisioning", "Repository"}, // includes stats for repositores and provisiong in general + Description: "Get resource stats for this namespace", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceStats"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: withTimeoutFunc(b.handleStats, 30*time.Second), + }, + { + Path: "settings", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + OperationId: "getFrontendSettings", // used for RTK client + // includes stats for repositores and provisiong in general + // This must include "Repository" so that the RTK client will invalidate when things are deleted + Tags: []string{"Provisioning", "Repository"}, + Description: "Get the frontend settings for this namespace", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RepositoryViewList"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: withTimeoutFunc(b.handleSettings, 30*time.Second), + }, + }, + } +} + +// TODO: why didn't we create a connector as we did before or have a separate file? +func (b *APIBuilder) handleStats(w http.ResponseWriter, r *http.Request) { + u, ok := authlib.AuthInfoFrom(r.Context()) + if !ok { + w.WriteHeader(400) + _, _ = w.Write([]byte("expected user")) + return + } + // TODO: check if lister could list too many repositories or resources + stats, err := b.resourceLister.Stats(r.Context(), u.GetNamespace(), "") + if err != nil { + errhttp.Write(r.Context(), err, w) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(stats) +} + +// TODO: why didn't we create a connector as we did before or have a separate file? +// TODO: is there a better way to provide a filtered view of the repositories to the frontend? +func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u, ok := authlib.AuthInfoFrom(ctx) + if !ok { + errhttp.Write(ctx, fmt.Errorf("expected user"), w) + return + } + + // TODO: check if lister could list too many repositories or resources + all, err := b.repositoryLister.Repositories(u.GetNamespace()).List(labels.Everything()) + if err != nil { + errhttp.Write(r.Context(), err, w) + return + } + + settings := provisioning.RepositoryViewList{ + Items: make([]provisioning.RepositoryView, len(all)), + // FIXME: this shouldn't be here in provisioning but at the dual writer or something about the storage + LegacyStorage: dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus), + } + for i, val := range all { + settings.Items[i] = provisioning.RepositoryView{ + Name: val.ObjectMeta.Name, + Title: val.Spec.Title, + Type: val.Spec.Type, + ReadOnly: len(val.Spec.Workflows) == 0, + Target: val.Spec.Sync.Target, + } + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(settings) +} diff --git a/pkg/registry/apis/provisioning/safepath/dir.go b/pkg/registry/apis/provisioning/safepath/dir.go new file mode 100644 index 00000000000..c6d0bbbe727 --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/dir.go @@ -0,0 +1,38 @@ +package safepath + +import ( + "path" + "strings" +) + +// IsDir returns true if the filePath ends with a slash. +// Empty string is considered a directory. +func IsDir(filePath string) bool { + if filePath == "" || filePath == "." { + return true + } + + return strings.HasSuffix(filePath, "/") +} + +// Dir behaves exactly as path.Dir, but returns "" for the root directory. +// and returns a trailing slash for all other directories. +func Dir(filePath string) string { + if filePath == "" { + return "" + } + + // Trim trailing slash before getting the directory + cleanPath := strings.TrimSuffix(filePath, "/") + dir := path.Dir(cleanPath) + if dir == "." || dir == "/" { + return "" + } + + return dir + "/" +} + +// InDir returns true if the filePath is a subdirectory of the given directory. +func InDir(filePath, dir string) bool { + return strings.HasPrefix(filePath, dir) +} diff --git a/pkg/registry/apis/provisioning/safepath/dir_test.go b/pkg/registry/apis/provisioning/safepath/dir_test.go new file mode 100644 index 00000000000..bf55b160660 --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/dir_test.go @@ -0,0 +1,162 @@ +package safepath + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsFolderPath(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "empty path", + filePath: "", + want: true, + }, + { + name: "dot path", + filePath: ".", + want: true, + }, + { + name: "file path without extension", + filePath: "test", + want: false, + }, + { + name: "file path with extension", + filePath: "test.json", + want: false, + }, + { + name: "folder path with trailing slash", + filePath: "folder/", + want: true, + }, + { + name: "nested folder path with trailing slash", + filePath: "folder/subfolder/", + want: true, + }, + { + name: "file path in folder without trailing slash", + filePath: "folder/test.json", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsDir(tt.filePath) + require.Equal(t, tt.want, got) + }) + } +} + +func TestDir(t *testing.T) { + tests := []struct { + name string + filePath string + want string + }{ + { + name: "empty path", + filePath: "", + want: "", + }, + { + name: "root path", + filePath: "/", + want: "", + }, + { + name: "single directory", + filePath: "folder", + want: "", + }, + { + name: "nested directory", + filePath: "folder/subfolder", + want: "folder/", + }, + { + name: "file in directory", + filePath: "folder/file.txt", + want: "folder/", + }, + { + name: "multiple nested directories", + filePath: "folder/subfolder/subsubfolder", + want: "folder/subfolder/", + }, + { + name: "directory with trailing slash", + filePath: "folder/subfolder/", + want: "folder/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Dir(tt.filePath) + require.Equal(t, tt.want, got) + }) + } +} + +func TestInDir(t *testing.T) { + tests := []struct { + name string + filePath string + dir string + want bool + }{ + { + name: "file in directory", + filePath: "folder/file.txt", + dir: "folder/", + want: true, + }, + { + name: "file not in directory", + filePath: "other/file.txt", + dir: "folder/", + want: false, + }, + { + name: "subdirectory", + filePath: "folder/subfolder/", + dir: "folder/", + want: true, + }, + { + name: "empty directory", + filePath: "folder/file.txt", + dir: "", + want: true, + }, + { + name: "exact match", + filePath: "folder/", + dir: "folder/", + want: true, + }, + { + name: "partial directory name match", + filePath: "folder2/file.txt", + dir: "folder/", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := InDir(tt.filePath, tt.dir) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/registry/apis/provisioning/safepath/path.go b/pkg/registry/apis/provisioning/safepath/path.go index 17f0adb18f9..d314fa80cdf 100644 --- a/pkg/registry/apis/provisioning/safepath/path.go +++ b/pkg/registry/apis/provisioning/safepath/path.go @@ -4,56 +4,69 @@ import ( "os" "path" "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -// ErrUnsafePathTraversal indicates that an input path had a path traversal which led to escaping the required prefix. -// E.g. Join("/test", "..") would return this, because it doesn't stay within the '/test' directory. -var ErrUnsafePathTraversal = apierrors.NewBadRequest("the input path had an unacceptable path traversal") - -// Join joins any number of elements in a path under a common prefix path. -// If the elems do path traversal, they are permitted to do so under their own directories. -// The output result will _always_ have a prefix of the given prefix, and no path traversals in the output string. -// The output result will not end with a trailing slash. -// The output result will have a leading slash if one is given as a prefix. -// If the prefix would ultimately be escaped, an error is returned. -// -// This function is safe for . -func Join(prefix string, elem ...string) (string, error) { - // We clean early to make the HasPrefix check be sensible after path.Join does a Clean for us. - prefix = replaceOSSeparators(path.Clean(prefix)) - if len(elem) == 0 { - return prefix, nil - } - - for i, e := range elem { - // We don't use Clean here because the output of path.Join will clean for us. - elem[i] = replaceOSSeparators(e) - } - subPath := path.Join(elem...) // performs a Clean after joining - completePath := path.Join(prefix, subPath) - if !strings.HasPrefix(completePath, prefix) { - return "", ErrUnsafePathTraversal - } - return completePath, nil -} - -// Performs a [path.Clean] on the path, as well as replacing its OS separators. -// Note that this does no effort to ensure the paths are safe to use. It only cleans them. -func Clean(p string) string { - return path.Clean(replaceOSSeparators(p)) -} +// TODO: explore if we want to use our own type for safepath +// to make it clearer that this is a safe path and not a regular path // osSeparator is declared as a var here only to ensure we can change it in tests. var osSeparator = os.PathSeparator +// Performs a [path.Clean] on the path, as well as replacing its OS separators. // This replaces the OS separator with a slash. // All OSes we target (Linux, macOS, and Windows) support forward-slashes in path traversals, as such it's simpler to use the same character everywhere. // BSDs do as well (even though they're not a target as of writing). -func replaceOSSeparators(p string) string { +func Clean(p string) string { if osSeparator == '/' { // perf: nothing to do! + return path.Clean(p) + } + + return path.Clean(strings.ReplaceAll(p, string(osSeparator), "/")) +} + +// Join is like path.Join but preserves trailing slashes from the last element +func Join(elem ...string) string { + if len(elem) == 0 { + return "" + } + + joined := path.Join(elem...) + // Preserve trailing slash if the last element had one + if strings.HasSuffix(elem[len(elem)-1], "/") { + return joined + "/" + } + + return joined +} + +// Base returns the last element of the path. +func Base(p string) string { + b := path.Base(p) + if b == "." || b == "/" { + return "" + } + + return b +} + +// RemoveExt returns the path without the extension. +// It should not remove the dot if the filename is e.g. `.gitignore` +func RemoveExt(p string) string { + // Special case: if the file starts with a dot and has no other dots, + // it's a hidden file and should not have its "extension" removed + base := Base(p) + if strings.HasPrefix(base, ".") && strings.Count(base, ".") == 1 { return p } - return strings.ReplaceAll(p, string(osSeparator), "/") + + ext := path.Ext(p) + if ext == "" { + return p + } + + return p[0 : len(p)-len(ext)] +} + +func IsAbs(p string) bool { + return path.IsAbs(p) } diff --git a/pkg/registry/apis/provisioning/safepath/path_test.go b/pkg/registry/apis/provisioning/safepath/path_test.go index abd4f14abc1..d42bde6296e 100644 --- a/pkg/registry/apis/provisioning/safepath/path_test.go +++ b/pkg/registry/apis/provisioning/safepath/path_test.go @@ -7,41 +7,26 @@ import ( ) func TestPathJoin(t *testing.T) { - orig := osSeparator - osSeparator = '\\' // pretend we're on Windows - defer func() { osSeparator = orig }() - testCases := []struct { Comment string In []string Out any // string or error }{ - {"Empty elements should not change input", []string{"/test/"}, "/test"}, - {"Empty elements without leading slash should not change input", []string{"test/"}, "test"}, + {"Empty elements should not change input", []string{"/test/"}, "/test/"}, + {"Empty elements without leading slash should not change input", []string{"test/"}, "test/"}, {"Single element should be added to path", []string{"/test/", "abc"}, "/test/abc"}, {"Single element should be added to path with current dir prefix", []string{"./test/", "abc"}, "test/abc"}, {"Single element with leading slash should be added to path", []string{"/test/", "/abc"}, "/test/abc"}, {"Many elements are all appended to path", []string{"/test/", "a", "b", "c"}, "/test/a/b/c"}, {"Path traversal within same directory should be expanded", []string{"/test/", "a", "..", "b", ".", "..", "c"}, "/test/c"}, - {"Path traversal escaping root dir prefix should return err", []string{"/test/", ".."}, ErrUnsafePathTraversal}, - {"Path traversal escaping no dir prefix should return err", []string{"test/", ".."}, ErrUnsafePathTraversal}, - {"Path traversal escaping current dir prefix should return err", []string{"./test/", ".."}, ErrUnsafePathTraversal}, - {"Complex path traversal escaping prefix should return err", []string{"/test/", "a/..///c/", "../../test/d/../a/../.."}, ErrUnsafePathTraversal}, - {"Complex path traversal remaining in prefix should be expanded", []string{"/test/", "a/..///c/", "../../test/d/"}, "/test/d"}, - {"Problematic code example from the g304 website", []string{"/safe/path", "../../private/path"}, ErrUnsafePathTraversal}, - {"Traversing beyond root should be expanded", []string{"/test/", "/../a"}, "/test/a"}, - {"OS separator should be replaced with a slash", []string{"/test\\test", "abc\\test"}, "/test/test/abc/test"}, + {"Complex path traversal remaining in prefix should be expanded", []string{"/test/", "a/..///c/", "../../test/d/"}, "/test/d/"}, } for _, tc := range testCases { tc := tc t.Run(tc.Comment, func(t *testing.T) { - path, err := Join(tc.In[0], tc.In[1:]...) - if ee, ok := tc.Out.(error); ok { - assert.ErrorIs(t, err, ee, "expected unsuccessful outcome") - assert.Empty(t, path, "expected empty string when unsuccessful") - } else if str, ok := tc.Out.(string); ok { - assert.NoError(t, err, "expected successful outcome") + path := Join(tc.In...) + if str, ok := tc.Out.(string); ok { assert.Equal(t, str, path) } else { panic("expected out was neither string nor error") @@ -63,6 +48,13 @@ func TestPathClean(t *testing.T) { {"Simple path", "/test/", "/test"}, {"Simple path with OS separators", "\\test\\here", "/test/here"}, {"Simple path with mixed separators", "\\test/here", "/test/here"}, + {"Path traversal within directory", "/test/abc/../def", "/test/def"}, + {"Multiple path traversals", "/test/abc/../../def", "/def"}, + {"Path traversal beyond root", "/test/../../../def", "/def"}, + {"Complex path traversal with mixed separators", "\\test\\abc\\..\\..\\def/ghi\\..", "/def"}, + {"Path traversal with multiple slashes", "/test////abc/..//def", "/test/def"}, + {"Path traversal with current directory", "/test/./abc/../def/./ghi", "/test/def/ghi"}, + {"Empty path segments with traversal", "//test//abc//..//def", "/test/def"}, } for _, tc := range testCases { @@ -72,3 +64,105 @@ func TestPathClean(t *testing.T) { }) } } + +func TestBase(t *testing.T) { + testCases := []struct { + name string + path string + expected string + }{ + { + name: "empty path", + path: "", + expected: "", + }, + { + name: "root path", + path: "/", + expected: "", + }, + { + name: "current directory", + path: ".", + expected: "", + }, + { + name: "simple filename", + path: "file.txt", + expected: "file.txt", + }, + { + name: "path with directory", + path: "/path/to/file.txt", + expected: "file.txt", + }, + { + name: "path with trailing slash", + path: "/path/to/dir/", + expected: "dir", + }, + { + name: "hidden file", + path: ".gitignore", + expected: ".gitignore", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := Base(tc.path) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestRemoveExt(t *testing.T) { + testCases := []struct { + name string + path string + expected string + }{ + { + name: "empty path", + path: "", + expected: "", + }, + { + name: "no extension", + path: "filename", + expected: "filename", + }, + { + name: "simple extension", + path: "file.txt", + expected: "file", + }, + { + name: "multiple dots", + path: "file.tar.gz", + expected: "file.tar", + }, + { + name: "hidden file", + path: ".gitignore", + expected: ".gitignore", + }, + { + name: "path with directory", + path: "/path/to/file.txt", + expected: "/path/to/file", + }, + { + name: "path with trailing slash", + path: "/path/to/dir/", + expected: "/path/to/dir/", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := RemoveExt(tc.path) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/pkg/registry/apis/provisioning/safepath/safe.go b/pkg/registry/apis/provisioning/safepath/safe.go new file mode 100644 index 00000000000..9e11220e00c --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/safe.go @@ -0,0 +1,81 @@ +package safepath + +import ( + "errors" + "regexp" + "strings" +) + +var ( + ErrPathTooLong = errors.New("path too long") + ErrInvalidCharacters = errors.New("path contains invalid characters") + ErrDoubleSlash = errors.New("path contains double slashes") + ErrInvalidFormat = errors.New("invalid path format") + ErrPercentChar = errors.New("path contains percent character which could be used for URL encoding attacks") + ErrHiddenPath = errors.New("path contains hidden file or directory (starting with dot)") + ErrPathTraversalAttempt = errors.New("path contains traversal attempt (./ or ../)") +) + +const ( + MaxPathLength = 1024 // Maximum allowed path length in characters +) + +// validPathPattern matches valid path characters: +// - Alphanumeric (a-z, A-Z, 0-9) +// - Forward slash for path separation +// - Dots for file extensions and current directory +// - Underscores and hyphens for file/folder names +var validPathPattern = regexp.MustCompile(`^[a-zA-Z0-9/_.-]+$`) + +func IsSafe(path string) error { + // Check path length + if len(path) > MaxPathLength { + return ErrPathTooLong + } + + // Empty path is valid (represents current directory) + if path == "" { + return nil + } + + // Check specifically for percent character first + if strings.Contains(path, "%") { + return ErrPercentChar + } + + // Check for invalid characters + if !validPathPattern.MatchString(path) { + return ErrInvalidCharacters + } + + // Check for double slashes + if strings.Contains(path, "//") { + return ErrDoubleSlash + } + + parts := Split(path) + + // Check for path traversal attempts first + for _, part := range parts { + if part == ".." || part == "." { + return ErrPathTraversalAttempt + } + } + + // Check for hidden files/directories in any part of the path + for _, part := range parts { + if part != "" && part[0] == '.' && part != ".." && part != "." { + return ErrHiddenPath + } + } + + // If it's not a directory, it should have a filename component + if !IsDir(path) && len(parts) > 0 { + filename := parts[len(parts)-1] + if filename == "" { + return ErrInvalidFormat + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/safepath/safe_test.go b/pkg/registry/apis/provisioning/safepath/safe_test.go new file mode 100644 index 00000000000..3f99a83b9da --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/safe_test.go @@ -0,0 +1,235 @@ +package safepath + +import ( + "errors" + "strings" + "testing" +) + +func TestIsSafe(t *testing.T) { + tests := []struct { + name string + path string + wantErr error + }{ + // Valid paths + { + name: "valid simple path", + path: "path/to/resource", + wantErr: nil, + }, + { + name: "valid path with extension", + path: "path/to/file.json", + wantErr: nil, + }, + { + name: "valid directory path with trailing slash", + path: "path/to/folder/", + wantErr: nil, + }, + { + name: "valid path with allowed special chars", + path: "my-path/to_file/resource.json", + wantErr: nil, + }, + { + name: "empty path", + path: "", + wantErr: nil, + }, + { + name: "path at max length", + path: strings.Repeat("a", MaxPathLength), + wantErr: nil, + }, + { + name: "valid directory", + path: "path/to/", + wantErr: nil, + }, + { + name: "valid path with dots in filename", + path: "path/to/file.min.js", + wantErr: nil, + }, + // Length and depth limits + { + name: "path too long", + path: strings.Repeat("a/", 512) + "file", // Creates path > MaxPathLength + wantErr: ErrPathTooLong, + }, + // Invalid characters and formats + { + name: "invalid special character hash", + path: "path/to/file#.json", + wantErr: ErrInvalidCharacters, + }, + { + name: "invalid character space", + path: "path/to/my file.json", + wantErr: ErrInvalidCharacters, + }, + { + name: "invalid character backslash", + path: "path\\to\\file.json", + wantErr: ErrInvalidCharacters, + }, + { + name: "invalid character question mark", + path: "path/to/file?.json", + wantErr: ErrInvalidCharacters, + }, + { + name: "invalid character asterisk", + path: "path/to/*.json", + wantErr: ErrInvalidCharacters, + }, + + // Double slashes + { + name: "double slashes in middle", + path: "path//to/file.json", + wantErr: ErrDoubleSlash, + }, + { + name: "double slashes at start", + path: "//path/to/file.json", + wantErr: ErrDoubleSlash, + }, + { + name: "double slashes at end", + path: "path/to/file//", + wantErr: ErrDoubleSlash, + }, + + // Hidden files and directories + { + name: "hidden file", + path: "path/to/.hidden", + wantErr: ErrHiddenPath, + }, + { + name: "hidden directory", + path: "path/to/.git/", + wantErr: ErrHiddenPath, + }, + { + name: "hidden file with extension", + path: "path/to/.gitignore", + wantErr: ErrHiddenPath, + }, + { + name: "hidden path component in middle", + path: "path/.hidden/file.json", + wantErr: ErrHiddenPath, + }, + { + name: "hidden path at root", + path: ".env/config.json", + wantErr: ErrHiddenPath, + }, + + // Path traversal attempts + { + name: "path traversal with parent directory", + path: "path/to/../file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "path traversal at start", + path: "../path/file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "path traversal with multiple levels", + path: "path/../../file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "path traversal at end", + path: "path/to/folder/../", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "single dot path component", + path: "path/to/./file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "double dot path component", + path: "path/to/../", + wantErr: ErrPathTraversalAttempt, + }, + + // Current directory references + { + name: "current directory at start", + path: "./path/file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "current directory in middle", + path: "path/./file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "current directory at end", + path: "path/to/./", + wantErr: ErrPathTraversalAttempt, + }, + + // URL encoding attempts + { + name: "percent character in filename", + path: "path/to/%20file.json", + wantErr: ErrPercentChar, + }, + { + name: "url encoded slash", + path: "path/to%2Ffile.json", + wantErr: ErrPercentChar, + }, + { + name: "url encoded dot", + path: "path/to%2E%2E/file.json", + wantErr: ErrPercentChar, + }, + { + name: "url encoded path traversal", + path: "path/to/%2e%2e/file.json", + wantErr: ErrPercentChar, + }, + { + name: "url encoded null byte", + path: "path/to/file%00.json", + wantErr: ErrPercentChar, + }, + + // Mixed invalid patterns + { + name: "mixed traversal attempts", + path: "./path/../file.json", + wantErr: ErrPathTraversalAttempt, + }, + { + name: "mixed special chars and traversal", + path: "../path/#/file.json", + wantErr: ErrInvalidCharacters, + }, + { + name: "mixed percent and special chars", + path: "path/%20/#/file.json", + wantErr: ErrPercentChar, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := IsSafe(tt.path) + if !errors.Is(err, tt.wantErr) { + t.Errorf("IsSafe() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/safepath/trie.go b/pkg/registry/apis/provisioning/safepath/trie.go new file mode 100644 index 00000000000..7b49699c4cf --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/trie.go @@ -0,0 +1,98 @@ +// Package safepath provides utilities for safe path handling and validation +// through a trie-based implementation. +package safepath + +import ( + "fmt" +) + +// trieNode represents a single node in the trie data structure. +type trieNode struct { + children map[string]*trieNode + isDir bool // marks if this node represents a directory +} + +// Trie implements a trie data structure for efficient path lookups and validation. +type Trie struct { + root *trieNode +} + +// NewTrie creates and returns a new initialized Trie. +func NewTrie() *Trie { + return &Trie{ + root: &trieNode{ + children: make(map[string]*trieNode), + }, + } +} + +// Add inserts a path into the trie. It returns an error if there's a conflict +// between the path types (file vs directory) or if the path is invalid. +func (t *Trie) Add(path string) error { + if path == "" || path == "/" { + return nil + } + + current := t.root + segments := Split(path) + + var accumulatedPath string + for i, segment := range segments { + accumulatedPath = Join(accumulatedPath, segment) + if current.children == nil { + current.children = make(map[string]*trieNode) + } + + isLastSegment := i == len(segments)-1 + node, exists := current.children[segment] + if !exists { + node = &trieNode{ + children: make(map[string]*trieNode), + } + current.children[segment] = node + } else { + if (!isLastSegment && !node.isDir) || (isLastSegment && !node.isDir && IsDir(path)) { + return fmt.Errorf("path %q exists but is not a directory", accumulatedPath) + } + + if isLastSegment && node.isDir && !IsDir(path) { + return fmt.Errorf("path %q exists but is not a file", accumulatedPath) + } + } + + current = node + current.isDir = !isLastSegment || IsDir(path) + } + + return nil +} + +// Exists checks if a path exists in the trie and matches its expected type (file/directory). +func (t *Trie) Exists(path string) bool { + if path == "" || path == "/" { + return true + } + + current := t.root + segments := Split(path) + + for i, segment := range segments { + if current.children == nil { + return false + } + + next, exists := current.children[segment] + if !exists { + return false + } + + current = next + isLastSegment := i == len(segments)-1 + + if isLastSegment { + return current.isDir == IsDir(path) + } + } + + return false +} diff --git a/pkg/registry/apis/provisioning/safepath/trie_test.go b/pkg/registry/apis/provisioning/safepath/trie_test.go new file mode 100644 index 00000000000..8daf93bc121 --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/trie_test.go @@ -0,0 +1,125 @@ +package safepath + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTrie(t *testing.T) { + tests := []struct { + name string + pathsToAdd []string + pathsToCheck []string + expectedExist []bool + expectedError error + }{ + { + name: "empty trie", + pathsToAdd: []string{}, + pathsToCheck: []string{"test", "test/"}, + expectedExist: []bool{false, false}, + expectedError: nil, + }, + { + name: "single file", + pathsToAdd: []string{"test.json"}, + pathsToCheck: []string{"test.json", "test.json/"}, + expectedExist: []bool{true, false}, + expectedError: nil, + }, + { + name: "single directory", + pathsToAdd: []string{"test/"}, + pathsToCheck: []string{"test", "test/"}, + expectedExist: []bool{false, true}, + expectedError: nil, + }, + { + name: "nested structure", + pathsToAdd: []string{"folder/", "folder/file.txt", "folder/subfolder/", "folder/subfolder/test.json"}, + pathsToCheck: []string{"folder/", "folder/file.txt", "folder/file.txt/", "folder/subfolder/", "folder/subfolder/test.json", "folder/subfolder/test.json/"}, + expectedExist: []bool{true, true, false, true, true, false}, + expectedError: nil, + }, + { + name: "partial paths", + pathsToAdd: []string{"a/b/c/d/"}, + pathsToCheck: []string{"a/", "a/b/", "a/b/c/", "a/b/c/d/"}, + expectedExist: []bool{true, true, true, true}, + expectedError: nil, + }, + { + name: "file in middle of path", + pathsToAdd: []string{"a/file.txt", "a/file.txt/b/"}, + pathsToCheck: []string{}, + expectedExist: []bool{}, + expectedError: fmt.Errorf("path %q exists but is not a directory", "a/file.txt"), + }, + { + name: "empty path", + pathsToAdd: []string{""}, + pathsToCheck: []string{""}, + expectedExist: []bool{true}, + expectedError: nil, + }, + { + name: "root directory", + pathsToAdd: []string{"/"}, + pathsToCheck: []string{"/", ""}, + expectedExist: []bool{true, true}, + expectedError: nil, + }, + { + name: "duplicate paths", + pathsToAdd: []string{"test/", "test/"}, + pathsToCheck: []string{"test/"}, + expectedExist: []bool{true}, + expectedError: nil, + }, + { + name: "file to directory conversion not allowed", + pathsToAdd: []string{"test.txt", "test.txt/file.txt"}, + pathsToCheck: []string{}, + expectedExist: []bool{}, + expectedError: fmt.Errorf("path %q exists but is not a directory", "test.txt"), + }, + { + name: "directory to file conversion not allowed", + pathsToAdd: []string{"test/", "test"}, + pathsToCheck: []string{}, + expectedExist: []bool{}, + expectedError: fmt.Errorf("path %q exists but is not a file", "test"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + trie := NewTrie() + + // Add paths + var lastErr error + for _, path := range tt.pathsToAdd { + err := trie.Add(path) + if err != nil { + lastErr = err + break + } + } + + if tt.expectedError != nil { + require.Error(t, lastErr) + require.Equal(t, tt.expectedError.Error(), lastErr.Error()) + return + } + require.NoError(t, lastErr) + + // Check existence + for i, path := range tt.pathsToCheck { + exists := trie.Exists(path) + require.Equal(t, tt.expectedExist[i], exists, "path: %s", path) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/safepath/walk.go b/pkg/registry/apis/provisioning/safepath/walk.go index 57a6c4a9bb6..048745da188 100644 --- a/pkg/registry/apis/provisioning/safepath/walk.go +++ b/pkg/registry/apis/provisioning/safepath/walk.go @@ -29,3 +29,17 @@ func Walk(ctx context.Context, p string, fn WalkFunc) error { return nil } + +// Depth returns the depth of the given path. +func Depth(p string) int { + return len(Split(p)) +} + +// Split splits the given path into segments. +func Split(p string) []string { + trimmed := strings.Trim(p, "/") + if trimmed == "" { + return []string{} + } + return strings.Split(trimmed, "/") +} diff --git a/pkg/registry/apis/provisioning/safepath/walk_test.go b/pkg/registry/apis/provisioning/safepath/walk_test.go new file mode 100644 index 00000000000..c1694f2af53 --- /dev/null +++ b/pkg/registry/apis/provisioning/safepath/walk_test.go @@ -0,0 +1,178 @@ +package safepath + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWalk(t *testing.T) { + tests := []struct { + name string + path string + expectedPaths []string + expectError bool + }{ + { + name: "simple path", + path: "a/b/c", + expectedPaths: []string{ + "a", + "a/b", + "a/b/c", + }, + }, + { + name: "path with leading slash", + path: "/a/b/c", + expectedPaths: []string{ + "a", + "a/b", + "a/b/c", + }, + }, + { + name: "path with trailing slash", + path: "a/b/c/", + expectedPaths: []string{ + "a", + "a/b", + "a/b/c", + }, + }, + { + name: "root path", + path: "/", + expectedPaths: nil, + }, + { + name: "current directory", + path: ".", + expectedPaths: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var paths []string + err := Walk(context.Background(), tt.path, func(ctx context.Context, p string) error { + paths = append(paths, p) + return nil + }) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedPaths, paths) + } + }) + } +} + +func TestDepth(t *testing.T) { + tests := []struct { + name string + path string + expectedDepth int + }{ + { + name: "empty path", + path: "", + expectedDepth: 0, + }, + { + name: "root path", + path: "/", + expectedDepth: 0, + }, + { + name: "single level", + path: "a", + expectedDepth: 1, + }, + { + name: "multiple levels", + path: "a/b/c", + expectedDepth: 3, + }, + { + name: "path with leading slash", + path: "/a/b/c", + expectedDepth: 3, + }, + { + name: "path with trailing slash", + path: "a/b/c/", + expectedDepth: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + depth := Depth(tt.path) + assert.Equal(t, tt.expectedDepth, depth) + }) + } +} + +func TestSplit(t *testing.T) { + tests := []struct { + name string + path string + expectedSegments []string + }{ + { + name: "empty path", + path: "", + expectedSegments: []string{}, + }, + { + name: "root path", + path: "/", + expectedSegments: []string{}, + }, + { + name: "single segment", + path: "a", + expectedSegments: []string{"a"}, + }, + { + name: "multiple segments", + path: "a/b/c", + expectedSegments: []string{"a", "b", "c"}, + }, + { + name: "path with leading slash", + path: "/a/b/c", + expectedSegments: []string{"a", "b", "c"}, + }, + { + name: "path with trailing slash", + path: "a/b/c/", + expectedSegments: []string{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + segments := Split(tt.path) + assert.Equal(t, tt.expectedSegments, segments) + }) + } +} + +func TestWalkError(t *testing.T) { + expectedErr := errors.New("test error") + err := Walk(context.Background(), "a/b/c", func(ctx context.Context, p string) error { + if p == "a/b" { + return expectedErr + } + return nil + }) + + require.ErrorIs(t, err, expectedErr) +} diff --git a/pkg/registry/apis/provisioning/sync.go b/pkg/registry/apis/provisioning/sync.go new file mode 100644 index 00000000000..3d5e6179a8f --- /dev/null +++ b/pkg/registry/apis/provisioning/sync.go @@ -0,0 +1,87 @@ +package provisioning + +import ( + "context" + "net/http" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" +) + +// TODO: should we have merge migrate and sync connectors and have a single repository job connector? +type syncConnector struct { + repoGetter RepoGetter + jobs jobs.Queue +} + +func (*syncConnector) New() runtime.Object { + return &provisioning.Job{} +} + +func (*syncConnector) Destroy() {} + +func (*syncConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (c *syncConnector) ProducesObject(verb string) any { + return c.New() +} + +func (*syncConnector) ConnectMethods() []string { + return []string{http.MethodPost} +} + +func (*syncConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (c *syncConnector) Connect( + ctx context.Context, + name string, + opts runtime.Object, + responder rest.Responder, +) (http.Handler, error) { + repo, err := c.repoGetter.GetHealthyRepository(ctx, name) + if err != nil { + return nil, err + } + cfg := repo.Config() + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var options provisioning.SyncJobOptions + + if err := unmarshalJSON(r, defaultMaxBodySize, &options); err != nil { + responder.Error(apierrors.NewBadRequest("error decoding SyncJobOptions from request")) + return + } + + job, err := c.jobs.Insert(ctx, &provisioning.Job{ + ObjectMeta: v1.ObjectMeta{ + Namespace: cfg.Namespace, + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionSync, + Repository: cfg.Name, + Pull: &options, + }, + }) + if err != nil { + responder.Error(err) + return + } + responder.Object(http.StatusAccepted, job) + }), 30*time.Second), nil +} + +var ( + _ rest.Connecter = (*syncConnector)(nil) + _ rest.Storage = (*syncConnector)(nil) + _ rest.StorageMetadata = (*syncConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/test.go b/pkg/registry/apis/provisioning/test.go new file mode 100644 index 00000000000..6b26e8482d6 --- /dev/null +++ b/pkg/registry/apis/provisioning/test.go @@ -0,0 +1,140 @@ +package provisioning + +import ( + "context" + "encoding/json" + "net/http" + "reflect" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + client "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +type testConnector struct { + getter RepoGetter +} + +func (*testConnector) New() runtime.Object { + return &provisioning.TestResults{} +} + +func (*testConnector) Destroy() {} + +func (*testConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (*testConnector) ProducesObject(verb string) any { + return &provisioning.TestResults{} +} + +func (*testConnector) ConnectMethods() []string { + return []string{http.MethodPost} +} + +func (*testConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := readBody(r, defaultMaxBodySize) + if err != nil { + responder.Error(err) + return + } + var repo repository.Repository + if len(body) > 0 { + var cfg provisioning.Repository + err = json.Unmarshal(body, &cfg) + if err != nil { + responder.Error(err) + return + } + + // In case the body is an empty object + if !reflect.ValueOf(cfg).IsZero() { + // Create a temporary repository + tmp, err := s.getter.AsRepository(ctx, &cfg) + if err != nil { + responder.Error(err) + return + } + + // TODO: Explore how to better support synchronous validation for the UI (and likely remove this hack) + if name != "new" { + repo, err = s.getter.AsRepository(ctx, &cfg) + if err != nil { + responder.Error(err) + return + } + + // Make sure we are OK with the changes + if cfg.Spec.Type != repo.Config().Spec.Type { + responder.Error(apierrors.NewBadRequest("test config must be the same type")) + return + } + } + repo = tmp + } + } + + if repo == nil { + repo, err = s.getter.GetRepository(ctx, name) + if err != nil { + responder.Error(err) + return + } + } + + // Only call test if field validation passes + rsp, err := repository.TestRepository(ctx, repo) + if err != nil { + responder.Error(err) + return + } + responder.Object(rsp.Code, rsp) + }), 30*time.Second), nil +} + +// TODO: Move tester to a more suitable location out of the connector. +type RepositoryTester struct { + // Repository+Jobs + client client.ProvisioningV0alpha1Interface +} + +// This function will check if the repository is configured and functioning as expected +func (t *RepositoryTester) UpdateHealthStatus(ctx context.Context, cfg *provisioning.Repository, res *provisioning.TestResults) (*provisioning.Repository, error) { + if res == nil { + res = &provisioning.TestResults{ + Success: false, + Errors: []string{ + "missing health status", + }, + } + } + + repo := cfg.DeepCopy() + repo.Status.Health = provisioning.HealthStatus{ + Healthy: res.Success, + Checked: time.Now().UnixMilli(), + Message: res.Errors, + } + + _, err := t.client.Repositories(repo.GetNamespace()). + UpdateStatus(ctx, repo, metav1.UpdateOptions{}) + return repo, err +} + +var ( + _ rest.Storage = (*testConnector)(nil) + _ rest.Connecter = (*testConnector)(nil) + _ rest.StorageMetadata = (*testConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/timeout.go b/pkg/registry/apis/provisioning/timeout.go new file mode 100644 index 00000000000..254b94e86db --- /dev/null +++ b/pkg/registry/apis/provisioning/timeout.go @@ -0,0 +1,21 @@ +package provisioning + +import ( + "context" + "net/http" + "time" +) + +// withTimeout adds a timeout context to the request +func withTimeout(h http.Handler, timeout time.Duration) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer cancel() + h.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// withTimeoutFunc adds a timeout context to the request +func withTimeoutFunc(f func(w http.ResponseWriter, r *http.Request), timeout time.Duration) func(w http.ResponseWriter, r *http.Request) { + return withTimeout(http.HandlerFunc(f), timeout).ServeHTTP +} diff --git a/pkg/registry/apis/provisioning/timeout_test.go b/pkg/registry/apis/provisioning/timeout_test.go new file mode 100644 index 00000000000..78425e9f4bf --- /dev/null +++ b/pkg/registry/apis/provisioning/timeout_test.go @@ -0,0 +1,48 @@ +package provisioning + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestWithTimeout(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + w.WriteHeader(http.StatusGatewayTimeout) + case <-time.After(50 * time.Millisecond): + w.WriteHeader(http.StatusOK) + } + }) + + tests := []struct { + name string + timeout time.Duration + wantStatus int + }{ + { + name: "request completes", + timeout: 100 * time.Millisecond, + wantStatus: http.StatusOK, + }, + { + name: "request times out", + timeout: 10 * time.Millisecond, + wantStatus: http.StatusGatewayTimeout, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + withTimeout(handler, tt.timeout).ServeHTTP(w, r) + + if w.Code != tt.wantStatus { + t.Errorf("withTimeout() status = %v, want %v", w.Code, tt.wantStatus) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/types.go b/pkg/registry/apis/provisioning/types.go new file mode 100644 index 00000000000..b2d3acdf3fa --- /dev/null +++ b/pkg/registry/apis/provisioning/types.go @@ -0,0 +1,21 @@ +package provisioning + +import ( + "context" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +type RepoGetter interface { + // This gets a repository with the provided name in the namespace from ctx + GetRepository(ctx context.Context, name string) (repository.Repository, error) + + // This will return a healthy repository, or an error saying the repository is not healthy + GetHealthyRepository(ctx context.Context, name string) (repository.Repository, error) + + // Given a repository configuration, return it as a repository instance + // This will only error for un-recoverable system errors + // the repository instance may or may not be valid/healthy + AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error) +} diff --git a/pkg/registry/apis/provisioning/usage.go b/pkg/registry/apis/provisioning/usage.go new file mode 100644 index 00000000000..b0fef85e468 --- /dev/null +++ b/pkg/registry/apis/provisioning/usage.go @@ -0,0 +1,55 @@ +package provisioning + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/labels" + + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +func (b *APIBuilder) collectProvisioningStats(ctx context.Context) (map[string]any, error) { + m := map[string]any{} + if b.unified == nil { + return m, nil + } + + // FIXME: hardcoded to "default" for now -- it works for single tenant deployments + // we could discover the set of valid namespaces, but that would count everything for + // each instance in cloud. + // + // We could get namespaces from the list of repos below, but that could be zero + // while we still have resources managed by terraform, etc + ns := "default" + count, err := b.unified.CountManagedObjects(ctx, &resource.CountManagedObjectsRequest{ + Namespace: ns, + }) + if err != nil { + return m, err + } + counts := make(map[string]int, 10) + for _, v := range count.Items { + counts[v.Kind] = counts[v.Kind] + int(v.Count) + } + for k, v := range counts { + m[fmt.Sprintf("stats.managed_by.%s.count", k)] = v + } + + // Inspect all configs + repos, err := b.repositoryLister.List(labels.Everything()) + if err != nil { + return m, err + } + clear(counts) + for _, repo := range repos { + counts[string(repo.Spec.Type)] = counts[string(repo.Spec.Type)] + 1 + } + + // Count how many items of each repository type + for k, v := range counts { + m[fmt.Sprintf("stats.repository.%s.count", k)] = v + } + + return m, nil +} diff --git a/pkg/registry/apis/provisioning/webhook.go b/pkg/registry/apis/provisioning/webhook.go new file mode 100644 index 00000000000..34c60260269 --- /dev/null +++ b/pkg/registry/apis/provisioning/webhook.go @@ -0,0 +1,124 @@ +package provisioning + +import ( + "context" + "fmt" + "net/http" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +// Webhook endpoint max size (25MB) +// See https://docs.github.com/en/webhooks/webhook-events-and-payloads +const webhookMaxBodySize = 25 * 1024 * 1024 + +// This only works for github right now +type webhookConnector struct { + getter RepoGetter + jobs jobs.Queue + webhooksEnabled bool +} + +func (*webhookConnector) New() runtime.Object { + return &provisioning.WebhookResponse{} +} + +func (*webhookConnector) Destroy() {} + +func (*webhookConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (*webhookConnector) ProducesObject(verb string) any { + return &provisioning.WebhookResponse{} +} + +func (*webhookConnector) ConnectMethods() []string { + return []string{ + http.MethodPost, + http.MethodGet, // only useful for browser testing, should be removed + } +} + +func (*webhookConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + namespace := request.NamespaceValue(ctx) + ctx, _, err := identity.WithProvisioningIdentity(ctx, namespace) + if err != nil { + return nil, err + } + + // Get the repository with the worker identity (since the request user is likely anonymous) + repo, err := s.getter.GetHealthyRepository(ctx, name) + if err != nil { + return nil, err + } + + return withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := logging.FromContext(r.Context()).With("logger", "webhook-connector", "repo", name) + ctx := logging.Context(r.Context(), logger) + if !s.webhooksEnabled { + responder.Error(errors.NewBadRequest("webhooks are not enabled")) + return + } + + hooks, ok := repo.(repository.Hooks) + if !ok { + responder.Error(errors.NewBadRequest("the repository does not support webhooks")) + return + } + + // Limit the webhook request body size + r.Body = http.MaxBytesReader(w, r.Body, webhookMaxBodySize) + + rsp, err := hooks.Webhook(ctx, r) + if err != nil { + responder.Error(err) + return + } + if rsp == nil { + responder.Error(fmt.Errorf("expecting a response")) + return + } + if rsp.Job != nil { + // Add the job to the job queue + job := &provisioning.Job{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Labels: map[string]string{ + "repository": name, + }, + }, + Spec: *rsp.Job, + } + job, err := s.jobs.Insert(ctx, job) + if err != nil { + responder.Error(err) + return + } + responder.Object(rsp.Code, job) + return + } + responder.Object(rsp.Code, rsp) + }), 30*time.Second), nil +} + +var ( + _ rest.Storage = (*webhookConnector)(nil) + _ rest.Connecter = (*webhookConnector)(nil) + _ rest.StorageMetadata = (*webhookConnector)(nil) +) diff --git a/pkg/registry/apis/query/client/plugin.go b/pkg/registry/apis/query/client/plugin.go index 434f3b049f9..7301f819be7 100644 --- a/pkg/registry/apis/query/client/plugin.go +++ b/pkg/registry/apis/query/client/plugin.go @@ -2,7 +2,6 @@ package client import ( "context" - "errors" "fmt" "net/http" "sync" @@ -13,9 +12,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/apimachinery/identity" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/query/clientapi" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -26,6 +27,7 @@ import ( type pluginClient struct { pluginClient plugins.Client pCtxProvider *plugincontext.Provider + ac accesscontrol.AccessControl } type pluginRegistry struct { @@ -43,10 +45,11 @@ var _ clientapi.QueryDataClient = (*pluginClient)(nil) var _ query.DataSourceApiServerRegistry = (*pluginRegistry)(nil) // NewQueryClientForPluginClient creates a client that delegates to the internal plugins.Client stack -func NewQueryClientForPluginClient(p plugins.Client, ctx *plugincontext.Provider) clientapi.QueryDataClient { +func NewQueryClientForPluginClient(p plugins.Client, ctx *plugincontext.Provider, accessControl accesscontrol.AccessControl) clientapi.QueryDataClient { return &pluginClient{ pluginClient: p, pCtxProvider: ctx, + ac: accessControl, } } @@ -59,6 +62,17 @@ func NewDataSourceRegistryFromStore(pluginStore pluginstore.Store, } } +func (d *pluginClient) CanQueryDataSource(ctx context.Context, uid string) (bool, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return false, err + } + + requiredScope := "datasources:uid:" + uid + evaluate := accesscontrol.EvalPermission(datasources.ActionQuery, requiredScope) + return d.ac.Evaluate(ctx, user, evaluate) +} + // ExecuteQueryData implements QueryHelper. func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) (*backend.QueryDataResponse, error) { queries, dsRef, err := data.ToDataSourceQueries(req) @@ -69,17 +83,23 @@ func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) return nil, fmt.Errorf("expected single datasource request") } + canQuery, err := d.CanQueryDataSource(ctx, dsRef.UID) + if err != nil { + return nil, err + } + + if !canQuery { + status := metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusForbidden, + Message: "Access denied to the data source", + } + return nil, &apierrors.StatusError{ErrStatus: status} + } + // NOTE: this depends on uid unique across datasources settings, err := d.pCtxProvider.GetDataSourceInstanceSettings(ctx, dsRef.UID) if err != nil { - if errors.Is(err, datasources.ErrDataSourceNotFound) { - status := metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusNotFound, - Message: "datasource not found", - } - return nil, &apierrors.StatusError{ErrStatus: status} - } return nil, err } diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 44f15087832..83ca935a662 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -127,7 +127,7 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, builder, err := NewQueryAPIBuilder( features, &CommonDataSourceClientSupplier{ - Client: client.NewQueryClientForPluginClient(pluginClient, pCtxProvider), + Client: client.NewQueryClientForPluginClient(pluginClient, pCtxProvider, accessControl), }, ar, client.NewDataSourceRegistryFromStore(pluginStore, dataSourcesService), diff --git a/pkg/registry/apis/secret/accesscontrol.go b/pkg/registry/apis/secret/accesscontrol.go new file mode 100644 index 00000000000..e93fe22d1cc --- /dev/null +++ b/pkg/registry/apis/secret/accesscontrol.go @@ -0,0 +1,125 @@ +package secret + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" +) + +const ( + // SecureValues + ActionSecretSecureValuesCreate = "secret.securevalues:create" // CREATE. + ActionSecretSecureValuesWrite = "secret.securevalues:write" // UPDATE. + ActionSecretSecureValuesRead = "secret.securevalues:read" // GET + LIST. + ActionSecretSecureValuesDelete = "secret.securevalues:delete" // DELETE. + + // Keepers + ActionSecretKeepersCreate = "secret.keepers:create" // CREATE. + ActionSecretKeepersWrite = "secret.keepers:write" // UPDATE. + ActionSecretKeepersRead = "secret.keepers:read" // GET + LIST. + ActionSecretKeepersDelete = "secret.keepers:delete" // DELETE. +) + +var ( + ScopeProviderSecretSecureValues = accesscontrol.NewScopeProvider("secret.securevalues") + ScopeProviderSecretKeepers = accesscontrol.NewScopeProvider("secret.keepers") + + ScopeAllSecureValues = ScopeProviderSecretSecureValues.GetResourceAllScope() + ScopeAllKeepers = ScopeProviderSecretKeepers.GetResourceAllScope() +) + +func RegisterAccessControlRoles(service accesscontrol.Service) error { + // SecureValues + secureValuesReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:secret.securevalues:reader", + DisplayName: "Secrets Manager secure values reader", + Description: "Read and list secure values.", + Group: "Secrets Manager", + Permissions: []accesscontrol.Permission{ + { + Action: ActionSecretSecureValuesRead, + Scope: ScopeAllSecureValues, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + secureValuesWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:secret.securevalues:writer", + DisplayName: "Secrets Manager secure values writer", + Description: "Create, update and delete secure values.", + Group: "Secrets Manager", + Permissions: []accesscontrol.Permission{ + { + Action: ActionSecretSecureValuesCreate, + Scope: ScopeAllSecureValues, + }, + { + Action: ActionSecretSecureValuesRead, + Scope: ScopeAllSecureValues, + }, + { + Action: ActionSecretSecureValuesWrite, + Scope: ScopeAllSecureValues, + }, + { + Action: ActionSecretSecureValuesDelete, + Scope: ScopeAllSecureValues, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // Keepers + keepersReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:secret.keepers:reader", + DisplayName: "Secrets Manager keepers reader", + Description: "Read and list keepers.", + Group: "Secrets Manager", + Permissions: []accesscontrol.Permission{ + { + Action: ActionSecretKeepersRead, + Scope: ScopeAllKeepers, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + keepersWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:secret.keepers:writer", + DisplayName: "Secrets Manager keepers writer", + Description: "Create, update and delete keepers.", + Group: "Secrets Manager", + Permissions: []accesscontrol.Permission{ + { + Action: ActionSecretKeepersCreate, + Scope: ScopeAllKeepers, + }, + { + Action: ActionSecretKeepersRead, + Scope: ScopeAllKeepers, + }, + { + Action: ActionSecretKeepersWrite, + Scope: ScopeAllKeepers, + }, + { + Action: ActionSecretKeepersDelete, + Scope: ScopeAllKeepers, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + return service.DeclareFixedRoles( + secureValuesReader, secureValuesWriter, + keepersReader, keepersWriter, + ) +} diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go new file mode 100644 index 00000000000..164143ce423 --- /dev/null +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -0,0 +1,49 @@ +package contracts + +import "context" + +// EncryptionManager is an envelope encryption service in charge of encrypting/decrypting secrets. +type EncryptionManager interface { + // Encrypt MUST NOT be used within database transactions, it may cause database locks. + // 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, opt EncryptionOptions) ([]byte, error) + Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) + + RotateDataKeys(ctx context.Context, namespace string) error + ReEncryptDataKeys(ctx context.Context, namespace string) error +} + +type EncryptionOptions func() string + +// EncryptWithoutScope uses a root level data key for encryption (DEK), +// in other words this DEK is not bound to any specific scope (not attached to any user, org, etc.). +func EncryptWithoutScope() EncryptionOptions { + return func() string { + return "root" + } +} + +// EncryptWithScope uses a data key for encryption bound to some specific scope (i.e., user, org, etc.). +// Scope should look like "user:10", "org:1". +func EncryptWithScope(scope string) EncryptionOptions { + return func() string { + return scope + } +} + +type EncryptedValue struct { + UID string + Namespace string + EncryptedData []byte + Created int64 + Updated int64 +} + +type EncryptedValueStorage interface { + Create(ctx context.Context, namespace string, encryptedData []byte) (*EncryptedValue, error) + Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error + Get(ctx context.Context, namespace string, uid string) (*EncryptedValue, error) + Delete(ctx context.Context, namespace string, uid string) error +} diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go new file mode 100644 index 00000000000..2ba2cfb05dd --- /dev/null +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -0,0 +1,55 @@ +package contracts + +import ( + "context" + "errors" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" +) + +var ( + ErrKeeperNotFound = errors.New("keeper not found") +) + +// KeeperMetadataStorage is the interface for wiring and dependency injection. +type KeeperMetadataStorage interface { + Create(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) + Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.Keeper, error) + Update(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string) error + List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.KeeperList, error) +} + +// KeeperType represents the type of a Keeper. +type KeeperType string + +const ( + SQLKeeperType KeeperType = "sql" + AWSKeeperType KeeperType = "aws" + AzureKeeperType KeeperType = "azure" + GCPKeeperType KeeperType = "gcp" + HashiCorpKeeperType KeeperType = "hashicorp" +) + +// ExternalID represents either the secure value's GUID or ref (in case of external secret references). +// This is saved in the secure_value metadata storage as `external_id`. +// TODO: this does not belong in the k8s spec, but it is used by us internally. Place it somewhere appropriate. +type ExternalID string + +func (s ExternalID) String() string { + return string(s) +} + +// Keeper is the interface for secret keepers. +type Keeper interface { + Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, exposedValueOrRef string) (ExternalID, error) + Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID, exposedValueOrRef string) error + Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID) (secretv0alpha1.ExposedSecureValue, error) + Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID) error +} + +const ( + DefaultSQLKeeper = "kp-default-sql" +) diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go new file mode 100644 index 00000000000..a3109e1d44f --- /dev/null +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -0,0 +1,23 @@ +package contracts + +import ( + "context" + "errors" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" +) + +var ( + ErrSecureValueNotFound = errors.New("secure value not found") +) + +// SecureValueMetadataStorage is the interface for wiring and dependency injection. +type SecureValueMetadataStorage interface { + Create(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) + Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) + Update(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string) error + List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.SecureValueList, error) +} diff --git a/pkg/registry/apis/secret/register.go b/pkg/registry/apis/secret/register.go new file mode 100644 index 00000000000..9b2ccfe85f6 --- /dev/null +++ b/pkg/registry/apis/secret/register.go @@ -0,0 +1,265 @@ +package secret + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kube-openapi/pkg/common" + + claims "github.com/grafana/authlib/types" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/reststorage" + "github.com/grafana/grafana/pkg/services/accesscontrol" + authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +var ( + _ builder.APIGroupBuilder = (*SecretAPIBuilder)(nil) + _ builder.APIGroupMutation = (*SecretAPIBuilder)(nil) + _ builder.APIGroupValidation = (*SecretAPIBuilder)(nil) + _ builder.APIGroupRouteProvider = (*SecretAPIBuilder)(nil) +) + +type SecretAPIBuilder struct { + tracer tracing.Tracer + secureValueMetadataStorage contracts.SecureValueMetadataStorage + keeperMetadataStorage contracts.KeeperMetadataStorage + accessClient claims.AccessClient + decryptersAllowList map[string]struct{} +} + +func NewSecretAPIBuilder( + tracer tracing.Tracer, + secureValueMetadataStorage contracts.SecureValueMetadataStorage, + keeperMetadataStorage contracts.KeeperMetadataStorage, + accessClient claims.AccessClient, + decryptersAllowList map[string]struct{}, +) *SecretAPIBuilder { + return &SecretAPIBuilder{tracer, secureValueMetadataStorage, keeperMetadataStorage, accessClient, decryptersAllowList} +} + +func RegisterAPIService( + features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + apiregistration builder.APIRegistrar, + tracer tracing.Tracer, + secureValueMetadataStorage contracts.SecureValueMetadataStorage, + keeperMetadataStorage contracts.KeeperMetadataStorage, + accessClient claims.AccessClient, + accessControlService accesscontrol.Service, +) (*SecretAPIBuilder, error) { + // Skip registration unless opting into experimental apis and the secrets management app platform flag. + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || + !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { + return nil, nil + } + + if err := RegisterAccessControlRoles(accessControlService); err != nil { + return nil, fmt.Errorf("register secret access control roles: %w", err) + } + + builder := NewSecretAPIBuilder( + tracer, + secureValueMetadataStorage, + keeperMetadataStorage, + accessClient, + nil, // OSS does not need an allow list. + ) + + apiregistration.RegisterAPI(builder) + + return builder, nil +} + +// GetGroupVersion returns the tuple of `group` and `version` for the API which uniquely identifies it. +func (b *SecretAPIBuilder) GetGroupVersion() schema.GroupVersion { + return secretv0alpha1.SchemeGroupVersion +} + +// InstallSchema is called by the `apiserver` which exposes the defined kinds. +func (b *SecretAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + err := secretv0alpha1.AddKnownTypes(scheme, secretv0alpha1.VERSION) + if err != nil { + return err + } + + // Link this version to the internal representation. + // This is used for server-side-apply (PATCH), and avoids the error: + // "no kind is registered for the type" + err = secretv0alpha1.AddKnownTypes(scheme, runtime.APIVersionInternal) + if err != nil { + return err + } + + // Internal Kubernetes metadata API. Presumably to display the available APIs? + // e.g. http://localhost:3000/apis/secret.grafana.app/v0alpha1 + metav1.AddToGroupVersion(scheme, secretv0alpha1.SchemeGroupVersion) + + // This sets the priority in case we have multiple versions. + // By default Kubernetes will only let you use `kubectl get ` with one version. + // In case there are multiple versions, we'd need to pass the full path with the `--raw` flag. + if err := scheme.SetVersionPriority(secretv0alpha1.SchemeGroupVersion); err != nil { + return fmt.Errorf("scheme set version priority: %w", err) + } + + return nil +} + +// UpdateAPIGroupInfo is called when creating a generic API server for this group of kinds. +func (b *SecretAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + secureValueResource := secretv0alpha1.SecureValuesResourceInfo + keeperResource := secretv0alpha1.KeeperResourceInfo + + // rest.Storage is a generic interface for RESTful storage services. + // The constructors need to at least implement this interface, but will most likely implement + // other interfaces that equal to different operations like `get`, `list` and so on. + secureRestStorage := map[string]rest.Storage{ + // Default path for `securevalue`. + // The `reststorage.SecureValueRest` struct will implement interfaces for CRUDL operations on `securevalue`. + secureValueResource.StoragePath(): reststorage.NewSecureValueRest(b.secureValueMetadataStorage, secureValueResource), + + // The `reststorage.KeeperRest` struct will implement interfaces for CRUDL operations on `keeper`. + keeperResource.StoragePath(): reststorage.NewKeeperRest(b.keeperMetadataStorage, keeperResource), + } + + apiGroupInfo.VersionedResourcesStorageMap[secretv0alpha1.VERSION] = secureRestStorage + return nil +} + +// GetOpenAPIDefinitions, is this only for documentation? +func (b *SecretAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return secretv0alpha1.GetOpenAPIDefinitions +} + +// GetAuthorizer decides whether the request is allowed, denied or no opinion based on credentials and request attributes. +// Usually most resource are stored in folders (e.g. alerts, dashboards), which allows users to manage permissions at folder level, +// rather than at resource level which also has the benefit of lowering the load on AuthZ side, since instead of storing access to +// a single dashboard, you'd store access to all dashboards in a specific folder. +// For Secrets, this is not the case, but if we want to make it so, we need to update this ResourceAuthorizer to check the containing folder. +// If we ever want to do that, get guidance from IAM first as well. +func (b *SecretAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return authsvc.NewResourceAuthorizer(b.accessClient) +} + +// Register additional routes with the server. +func (b *SecretAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + return nil +} + +// Validate is called in `Create`, `Update` and `Delete` REST funcs, if the body calls the argument `rest.ValidateObjectFunc`. +func (b *SecretAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject() + operation := a.GetOperation() + + if obj == nil || operation == admission.Connect { + return nil // This is normal for sub-resource + } + + groupKind := obj.GetObjectKind().GroupVersionKind().GroupKind() + + // Generic validations for all kinds. At this point the name+namespace must not be empty. + if a.GetName() == "" { + return apierrors.NewInvalid( + groupKind, + a.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata", "name"), "a `name` is required")}, + ) + } + + if a.GetNamespace() == "" { + return apierrors.NewInvalid( + groupKind, + a.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "a `namespace` is required")}, + ) + } + + switch typedObj := obj.(type) { + case *secretv0alpha1.SecureValue: + var oldObj *secretv0alpha1.SecureValue + + if a.GetOldObject() != nil { + var ok bool + + oldObj, ok = a.GetOldObject().(*secretv0alpha1.SecureValue) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("old object is not a SecureValue, found %T", a.GetOldObject())) + } + } + + if errs := reststorage.ValidateSecureValue(typedObj, oldObj, operation, b.decryptersAllowList); len(errs) > 0 { + return apierrors.NewInvalid(groupKind, a.GetName(), errs) + } + + return nil + case *secretv0alpha1.Keeper: + if errs := reststorage.ValidateKeeper(typedObj, operation); len(errs) > 0 { + return apierrors.NewInvalid(groupKind, a.GetName(), errs) + } + + return nil + } + + return apierrors.NewBadRequest(fmt.Sprintf("unknown spec %T", obj)) +} + +func (b *SecretAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject() + operation := a.GetOperation() + + if obj == nil || operation == admission.Connect { + return nil // This is normal for sub-resource + } + + // When creating a resource and the name is empty, we need to generate one. + if operation == admission.Create && a.GetName() == "" { + generatedName, err := util.GetRandomString(8) + if err != nil { + return fmt.Errorf("generate random string: %w", err) + } + + switch typedObj := obj.(type) { + case *secretv0alpha1.SecureValue: + optionalPrefix := typedObj.GenerateName + if optionalPrefix == "" { + optionalPrefix = "sv-" + } + + typedObj.Name = optionalPrefix + generatedName + + case *secretv0alpha1.Keeper: + optionalPrefix := typedObj.GenerateName + if optionalPrefix == "" { + optionalPrefix = "kp-" + } + + typedObj.Name = optionalPrefix + generatedName + } + } + + // On any mutation to a `SecureValue`, override the `phase` as `Pending` and an empty `message`. + if operation == admission.Create || operation == admission.Update { + sv, ok := obj.(*secretv0alpha1.SecureValue) + if ok && sv != nil { + sv.Status.Phase = secretv0alpha1.SecureValuePhasePending + sv.Status.Message = "" + } + } + + return nil +} diff --git a/pkg/registry/apis/secret/reststorage/keeper_rest.go b/pkg/registry/apis/secret/reststorage/keeper_rest.go new file mode 100644 index 00000000000..77926723e78 --- /dev/null +++ b/pkg/registry/apis/secret/reststorage/keeper_rest.go @@ -0,0 +1,356 @@ +package reststorage + +import ( + "context" + "errors" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +var ( + _ rest.Scoper = (*KeeperRest)(nil) + _ rest.SingularNameProvider = (*KeeperRest)(nil) + _ rest.Getter = (*KeeperRest)(nil) + _ rest.Lister = (*KeeperRest)(nil) + _ rest.Storage = (*KeeperRest)(nil) + _ rest.Creater = (*KeeperRest)(nil) + _ rest.Updater = (*KeeperRest)(nil) + _ rest.GracefulDeleter = (*KeeperRest)(nil) +) + +// KeeperRest is an implementation of CRUDL operations on a `keeper` backed by TODO. +type KeeperRest struct { + storage contracts.KeeperMetadataStorage + resource utils.ResourceInfo + tableConverter rest.TableConvertor +} + +// NewKeeperRest is a returns a constructed `*KeeperRest`. +func NewKeeperRest(storage contracts.KeeperMetadataStorage, resource utils.ResourceInfo) *KeeperRest { + return &KeeperRest{storage, resource, resource.TableConverter()} +} + +// New returns an empty `*Keeper` that is used by the `Create` method. +func (s *KeeperRest) New() runtime.Object { + return s.resource.NewFunc() +} + +// Destroy is called when? [TODO] +func (s *KeeperRest) Destroy() {} + +// NamespaceScoped returns `true` because the storage is namespaced (== org). +func (s *KeeperRest) NamespaceScoped() bool { + return true +} + +// GetSingularName is used by `kubectl` discovery to have singular name representation of resources. +func (s *KeeperRest) GetSingularName() string { + return s.resource.GetSingularName() +} + +// NewList returns an empty `*KeeperList` that is used by the `List` method. +func (s *KeeperRest) NewList() runtime.Object { + return s.resource.NewListFunc() +} + +// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`. +func (s *KeeperRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +// List calls the inner `store` (persistence) and returns a list of `Keepers` within a `namespace` filtered by the `options`. +func (s *KeeperRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing namespace") + } + + keepersList, err := s.storage.List(ctx, xkube.Namespace(namespace), options) + if err != nil { + return nil, fmt.Errorf("failed to list keepers: %w", err) + } + + return keepersList, nil +} + +// Get calls the inner `store` (persistence) and returns a `Keeper` by `name`. +func (s *KeeperRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing namespace") + } + + kp, err := s.storage.Read(ctx, xkube.Namespace(namespace), name) + if err != nil { + if errors.Is(err, contracts.ErrKeeperNotFound) { + return nil, s.resource.NewNotFound(name) + } + return nil, fmt.Errorf("failed to read keeper: %w", err) + } + + return kp, nil +} + +// Create a new `Keeper`. Does some validation and allows empty `name` (generated). +func (s *KeeperRest) Create( + ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + options *metav1.CreateOptions, +) (runtime.Object, error) { + kp, ok := obj.(*secretv0alpha1.Keeper) + if !ok { + return nil, fmt.Errorf("expected Keeper for create") + } + + if err := createValidation(ctx, obj); err != nil { + return nil, err + } + + createdKeeper, err := s.storage.Create(ctx, kp) + if err != nil { + var kErr xkube.ErrorLister + if errors.As(err, &kErr) { + return nil, apierrors.NewInvalid(kp.GroupVersionKind().GroupKind(), kp.Name, kErr.ErrorList()) + } + + return nil, fmt.Errorf("failed to create keeper: %w", err) + } + + return createdKeeper, nil +} + +// Update a `Keeper`'s `value`. The second return parameter indicates whether the resource was newly created. +func (s *KeeperRest) Update( + ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + forceAllowCreate bool, + options *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + oldObj, err := s.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + + // Makes sure the UID and ResourceVersion are OK. + // TODO: this also makes it so the labels and annotations are additive, unless we check and remove manually. + newObj, err := objInfo.UpdatedObject(ctx, oldObj) + if err != nil { + return nil, false, fmt.Errorf("k8s updated object: %w", err) + } + + // The current supported behavior for `Update` is to replace the entire `spec` with the new one. + // Each provider-specific setting of a keeper lives at the top-level, so it makes it possible to change a provider + // during an update. Otherwise both old and new providers would be merged in the `newObj` which is not allowed. + if err := updateValidation(ctx, newObj, oldObj); err != nil { + return nil, false, err + } + + newKeeper, ok := newObj.(*secretv0alpha1.Keeper) + if !ok { + return nil, false, fmt.Errorf("expected Keeper for update") + } + + // TODO: do we need to do this here again? Probably not, but double-check! + newKeeper.Annotations = xkube.CleanAnnotations(newKeeper.Annotations) + + // Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that? + updatedKeeper, err := s.storage.Update(ctx, newKeeper) + if err != nil { + var kErr xkube.ErrorLister + if errors.As(err, &kErr) { + return nil, false, apierrors.NewInvalid(newKeeper.GroupVersionKind().GroupKind(), newKeeper.Name, kErr.ErrorList()) + } + + return nil, false, fmt.Errorf("failed to update keeper: %w", err) + } + + return updatedKeeper, false, nil +} + +// Delete calls the inner `store` (persistence) in order to delete the `Keeper`. +// The second return parameter `bool` indicates whether the delete was intant or not. It always is for `Keepers`. +func (s *KeeperRest) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, false, fmt.Errorf("missing namespace") + } + + if err := s.storage.Delete(ctx, xkube.Namespace(namespace), name); err != nil { + return nil, false, fmt.Errorf("failed to delete keeper: %w", err) + } + + return nil, true, nil +} + +// ValidateKeeper does basic spec validation of a keeper. +func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation) field.ErrorList { + // Only validate Create and Update for now. + if operation != admission.Create && operation != admission.Update { + return nil + } + + errs := make(field.ErrorList, 0) + + if keeper.Spec.Title == "" { + errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) + } + + // Only one keeper type can be configured. Return early and don't validate the specific keeper fields. + if err := validateKeepers(keeper); err != nil { + errs = append(errs, err) + + return errs + } + + // TODO: Improve SQL keeper validation. + // SQL keeper is not allowed to use `secureValueName` in credentials fields to avoid depending on another keeper. + if keeper.IsSqlKeeper() { + if keeper.Spec.SQL.Encryption.AWS != nil { + if keeper.Spec.SQL.Encryption.AWS.AccessKeyID.SecureValueName != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "accessKeyId"), "secureValueName cannot be used with SQL keeper")) + } + + if keeper.Spec.SQL.Encryption.AWS.SecretAccessKey.SecureValueName != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "secretAccessKey"), "secureValueName cannot be used with SQL keeper")) + } + } + + if keeper.Spec.SQL.Encryption.Azure != nil && keeper.Spec.SQL.Encryption.Azure.ClientSecret.SecureValueName != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec", "azure", "clientSecret"), "secureValueName cannot be used with SQL keeper")) + } + + if keeper.Spec.SQL.Encryption.HashiCorp != nil && keeper.Spec.SQL.Encryption.HashiCorp.Token.SecureValueName != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec", "hashicorp", "token"), "secureValueName cannot be used with SQL keeper")) + } + } + + if keeper.Spec.AWS != nil { + if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyId"), keeper.Spec.AWS.AccessKeyID); err != nil { + errs = append(errs, err) + } + + if err := validateCredentialValue(field.NewPath("spec", "aws", "secretAccessKey"), keeper.Spec.AWS.SecretAccessKey); err != nil { + errs = append(errs, err) + } + } + + if keeper.Spec.Azure != nil { + if keeper.Spec.Azure.KeyVaultName == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required")) + } + + if keeper.Spec.Azure.TenantID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantId"), "a `tenantId` is required")) + } + + if keeper.Spec.Azure.ClientID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientId"), "a `clientId` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), keeper.Spec.Azure.ClientSecret); err != nil { + errs = append(errs, err) + } + } + + if keeper.Spec.GCP != nil { + if keeper.Spec.GCP.ProjectID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectId"), "a `projectId` is required")) + } + + if keeper.Spec.GCP.CredentialsFile == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required")) + } + } + + if keeper.Spec.HashiCorp != nil { + if keeper.Spec.HashiCorp.Address == "" { + errs = append(errs, field.Required(field.NewPath("spec", "hashicorp", "address"), "a `address` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "hashicorp", "token"), keeper.Spec.HashiCorp.Token); err != nil { + errs = append(errs, err) + } + } + + return errs +} + +func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error { + availableKeepers := map[string]bool{ + "sql": keeper.Spec.SQL != nil, + "aws": keeper.Spec.AWS != nil, + "azure": keeper.Spec.Azure != nil, + "gcp": keeper.Spec.GCP != nil, + "hashicorp": keeper.Spec.HashiCorp != nil, + } + + configuredKeepers := make([]string, 0) + + for keeperKind, notNil := range availableKeepers { + if notNil { + configuredKeepers = append(configuredKeepers, keeperKind) + } + } + + if len(configuredKeepers) == 0 { + return field.Required(field.NewPath("spec"), "at least one `keeper` must be present") + } + + if len(configuredKeepers) > 1 { + return field.Invalid( + field.NewPath("spec"), + strings.Join(configuredKeepers, " & "), + "only one `keeper` can be present at a time but found more", + ) + } + + return nil +} + +func validateCredentialValue(path *field.Path, credentials secretv0alpha1.CredentialValue) *field.Error { + availableOptions := map[string]bool{ + "secureValueName": credentials.SecureValueName != "", + "valueFromEnv": credentials.ValueFromEnv != "", + "valueFromConfig": credentials.ValueFromConfig != "", + } + + configuredCredentials := make([]string, 0) + + for credentialKind, notEmpty := range availableOptions { + if notEmpty { + configuredCredentials = append(configuredCredentials, credentialKind) + } + } + + if len(configuredCredentials) == 0 { + return field.Required(path, "one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present") + } + + if len(configuredCredentials) > 1 { + return field.Invalid( + path, + strings.Join(configuredCredentials, " & "), + "only one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present at a time but found more", + ) + } + + return nil +} diff --git a/pkg/registry/apis/secret/reststorage/keeper_rest_test.go b/pkg/registry/apis/secret/reststorage/keeper_rest_test.go new file mode 100644 index 00000000000..abc1ac52b63 --- /dev/null +++ b/pkg/registry/apis/secret/reststorage/keeper_rest_test.go @@ -0,0 +1,332 @@ +package reststorage + +import ( + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/admission" +) + +func TestValidateKeeper(t *testing.T) { + t.Run("when creating a new keeper", func(t *testing.T) { + t.Run("the `title` must be present", func(t *testing.T) { + keeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + SQL: &secretv0alpha1.SQLKeeperConfig{}, + }, + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.title", errs[0].Field) + }) + }) + + t.Run("only one `keeper` must be present", func(t *testing.T) { + keeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + SQL: &secretv0alpha1.SQLKeeperConfig{}, + AWS: &secretv0alpha1.AWSKeeperConfig{}, + Azure: &secretv0alpha1.AzureKeeperConfig{}, + GCP: &secretv0alpha1.GCPKeeperConfig{}, + HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{}, + }, + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("at least one `keeper` must be present", func(t *testing.T) { + keeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + }, + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("aws keeper validation", func(t *testing.T) { + validKeeperAWS := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + AWS: &secretv0alpha1.AWSKeeperConfig{ + AWSCredentials: secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ + ValueFromEnv: "some-value", + }, + SecretAccessKey: secretv0alpha1.CredentialValue{ + SecureValueName: "some-value", + }, + KMSKeyID: "optional", + }, + }, + }, + } + + t.Run("`accessKeyId` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.AWS.AccessKeyID = secretv0alpha1.CredentialValue{} + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.accessKeyId", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.AWS.AccessKeyID = secretv0alpha1.CredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.accessKeyId", errs[0].Field) + }) + }) + + t.Run("`secretAccessKey` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.AWS.SecretAccessKey = secretv0alpha1.CredentialValue{} + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.AWS.SecretAccessKey = secretv0alpha1.CredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + }) + }) + }) + + t.Run("azure keeper validation", func(t *testing.T) { + validKeeperAzure := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + Azure: &secretv0alpha1.AzureKeeperConfig{ + AzureCredentials: secretv0alpha1.AzureCredentials{ + KeyVaultName: "kv-name", + TenantID: "tenant-id", + ClientID: "client-id", + ClientSecret: secretv0alpha1.CredentialValue{ + ValueFromConfig: "config.path.value", + }, + }, + }, + }, + } + + t.Run("`keyVaultName` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.KeyVaultName = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.keyVaultName", errs[0].Field) + }) + + t.Run("`tenantId` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.TenantID = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.tenantId", errs[0].Field) + }) + + t.Run("`clientId` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientID = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientId", errs[0].Field) + }) + + t.Run("`clientSecret` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientSecret = secretv0alpha1.CredentialValue{} + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientSecret", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientSecret = secretv0alpha1.CredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientSecret", errs[0].Field) + }) + }) + }) + + t.Run("gcp keeper validation", func(t *testing.T) { + validKeeperGCP := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + GCP: &secretv0alpha1.GCPKeeperConfig{ + GCPCredentials: secretv0alpha1.GCPCredentials{ + ProjectID: "project-id", + CredentialsFile: "/path/to/credentials/file.json", + }, + }, + }, + } + + t.Run("`projectId` must be present", func(t *testing.T) { + keeper := validKeeperGCP.DeepCopy() + keeper.Spec.GCP.ProjectID = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.gcp.projectId", errs[0].Field) + }) + + t.Run("`credentialsFile` must be present", func(t *testing.T) { + keeper := validKeeperGCP.DeepCopy() + keeper.Spec.GCP.CredentialsFile = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.gcp.credentialsFile", errs[0].Field) + }) + }) + + t.Run("hashicorp keeper validation", func(t *testing.T) { + validKeeperHashiCorp := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{ + HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{ + Address: "http://address", + Token: secretv0alpha1.CredentialValue{ + ValueFromConfig: "config.path.value", + }, + }, + }, + }, + } + + t.Run("`address` must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorp.Address = "" + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashicorp.address", errs[0].Field) + }) + + t.Run("`token` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorp.Token = secretv0alpha1.CredentialValue{} + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashicorp.token", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorp.Token = secretv0alpha1.CredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashicorp.token", errs[0].Field) + }) + }) + }) + + t.Run("sql keeper validation", func(t *testing.T) { + t.Run("does not allow usage of `secureValueName` in credentials", func(t *testing.T) { + providers := []struct { + name string + enc secretv0alpha1.Encryption + expectedErrors int + }{ + { + name: "aws", + enc: secretv0alpha1.Encryption{ + AWS: &secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ + SecureValueName: "not-empty", + }, + SecretAccessKey: secretv0alpha1.CredentialValue{ + SecureValueName: "not-empty", + }, + }, + }, + expectedErrors: 2, + }, + { + name: "azure", + enc: secretv0alpha1.Encryption{ + Azure: &secretv0alpha1.AzureCredentials{ + ClientSecret: secretv0alpha1.CredentialValue{ + SecureValueName: "not-empty", + }, + }, + }, + expectedErrors: 1, + }, + { + name: "hashicorp", + enc: secretv0alpha1.Encryption{ + HashiCorp: &secretv0alpha1.HashiCorpCredentials{ + Token: secretv0alpha1.CredentialValue{ + SecureValueName: "not-empty", + }, + }, + }, + expectedErrors: 1, + }, + } + + for _, tc := range providers { + t.Run("when using credentials for "+tc.name, func(t *testing.T) { + keeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Title: "title", + SQL: &secretv0alpha1.SQLKeeperConfig{Encryption: &tc.enc}, + }, + } + + errs := ValidateKeeper(keeper, admission.Create) + require.Len(t, errs, tc.expectedErrors) + }) + } + }) + }) +} diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest.go b/pkg/registry/apis/secret/reststorage/secure_value_rest.go new file mode 100644 index 00000000000..f0b043108e9 --- /dev/null +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest.go @@ -0,0 +1,315 @@ +package reststorage + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +var ( + _ rest.Scoper = (*SecureValueRest)(nil) + _ rest.SingularNameProvider = (*SecureValueRest)(nil) + _ rest.Getter = (*SecureValueRest)(nil) + _ rest.Lister = (*SecureValueRest)(nil) + _ rest.Storage = (*SecureValueRest)(nil) + _ rest.Creater = (*SecureValueRest)(nil) + _ rest.Updater = (*SecureValueRest)(nil) + _ rest.GracefulDeleter = (*SecureValueRest)(nil) +) + +// SecureValueRest is an implementation of CRUDL operations on a `securevalue` backed by a persistence layer `store`. +type SecureValueRest struct { + storage contracts.SecureValueMetadataStorage + resource utils.ResourceInfo + tableConverter rest.TableConvertor +} + +// NewSecureValueRest is a returns a constructed `*SecureValueRest`. +func NewSecureValueRest(storage contracts.SecureValueMetadataStorage, resource utils.ResourceInfo) *SecureValueRest { + return &SecureValueRest{storage, resource, resource.TableConverter()} +} + +// New returns an empty `*SecureValue` that is used by the `Create` method. +func (s *SecureValueRest) New() runtime.Object { + return s.resource.NewFunc() +} + +// Destroy is called when? [TODO] +func (s *SecureValueRest) Destroy() {} + +// NamespaceScoped returns `true` because the storage is namespaced (== org). +func (s *SecureValueRest) NamespaceScoped() bool { + return true +} + +// GetSingularName is used by `kubectl` discovery to have singular name representation of resources. +func (s *SecureValueRest) GetSingularName() string { + return s.resource.GetSingularName() +} + +// NewList returns an empty `*SecureValueList` that is used by the `List` method. +func (s *SecureValueRest) NewList() runtime.Object { + return s.resource.NewListFunc() +} + +// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`. +func (s *SecureValueRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +// List calls the inner `store` (persistence) and returns a list of `securevalues` within a `namespace` filtered by the `options`. +func (s *SecureValueRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing namespace") + } + + secureValueList, err := s.storage.List(ctx, xkube.Namespace(namespace), options) + if err != nil { + return nil, fmt.Errorf("failed to list secure values: %w", err) + } + + return secureValueList, nil +} + +// Get calls the inner `store` (persistence) and returns a `securevalue` by `name`. It will NOT return the decrypted `value`. +func (s *SecureValueRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing namespace") + } + + sv, err := s.storage.Read(ctx, xkube.Namespace(namespace), name) + if err != nil { + if errors.Is(err, contracts.ErrSecureValueNotFound) { + return nil, s.resource.NewNotFound(name) + } + + return nil, fmt.Errorf("failed to read secure value: %w", err) + } + + return sv, nil +} + +// Create a new `securevalue`. Does some validation and allows empty `name` (generated). +func (s *SecureValueRest) Create( + ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + options *metav1.CreateOptions, +) (runtime.Object, error) { + sv, ok := obj.(*secretv0alpha1.SecureValue) + if !ok { + return nil, fmt.Errorf("expected SecureValue for create") + } + + if err := createValidation(ctx, obj); err != nil { + return nil, err + } + + createdSecureValue, err := s.storage.Create(ctx, sv) + if err != nil { + return nil, fmt.Errorf("failed to create secure value: %w", err) + } + + return createdSecureValue, nil +} + +// Update a `securevalue`'s `value`. The second return parameter indicates whether the resource was newly created. +// Currently does not support "create on update" functionality. If the securevalue does not yet exist, it returns an error. +func (s *SecureValueRest) Update( + ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + forceAllowCreate bool, + options *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + oldObj, err := s.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + + // Makes sure the UID and ResourceVersion are OK. + // TODO: this also makes it so the labels and annotations are additive, unless we check and remove manually. + newObj, err := objInfo.UpdatedObject(ctx, oldObj) + if err != nil { + return nil, false, fmt.Errorf("k8s updated object: %w", err) + } + + if err := updateValidation(ctx, newObj, oldObj); err != nil { + return nil, false, err + } + + newSecureValue, ok := newObj.(*secretv0alpha1.SecureValue) + if !ok { + return nil, false, fmt.Errorf("expected SecureValue for update") + } + + // TODO: do we need to do this here again? Probably not, but double-check! + newSecureValue.Annotations = xkube.CleanAnnotations(newSecureValue.Annotations) + + // Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that? + updatedSecureValue, err := s.storage.Update(ctx, newSecureValue) + if err != nil { + return nil, false, fmt.Errorf("failed to update secure value: %w", err) + } + + return updatedSecureValue, false, nil +} + +// Delete calls the inner `store` (persistence) in order to delete the `securevalue`. +// The second return parameter `bool` indicates whether the delete was instant or not. It always is for `securevalues`. +func (s *SecureValueRest) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, false, fmt.Errorf("missing namespace") + } + + if err := s.storage.Delete(ctx, xkube.Namespace(namespace), name); err != nil { + return nil, false, fmt.Errorf("delete secure value: %w", err) + } + + return nil, true, nil +} + +// ValidateSecureValue does basic spec validation of a securevalue. +func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admission.Operation, decryptersAllowList map[string]struct{}) field.ErrorList { + errs := make(field.ErrorList, 0) + + // Operation-specific field validation. + switch operation { + case admission.Create: + errs = validateSecureValueCreate(sv) + + // If we plan to support PATCH-style updates, we shouldn't be requiring fields to be set. + case admission.Update: + errs = validateSecureValueUpdate(sv, oldSv) + + case admission.Delete: + case admission.Connect: + } + + // General validations. + if errs := validateDecrypters(sv.Spec.Decrypters, decryptersAllowList); len(errs) > 0 { + return errs + } + + return errs +} + +// validateSecureValueCreate does basic spec validation of a securevalue for the Create operation. +func validateSecureValueCreate(sv *secretv0alpha1.SecureValue) field.ErrorList { + errs := make(field.ErrorList, 0) + + if sv.Spec.Title == "" { + errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) + } + + if sv.Spec.Keeper == "" { + errs = append(errs, field.Required(field.NewPath("spec", "keeper"), "a `keeper` is required")) + } + + if sv.Spec.Value == "" && sv.Spec.Ref == "" { + errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required")) + } + + if sv.Spec.Value != "" && sv.Spec.Ref != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "only one of `value` or `ref` can be set")) + } + + return errs +} + +// validateSecureValueUpdate does basic spec validation of a securevalue for the Update operation. +func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.ErrorList { + errs := make(field.ErrorList, 0) + + // For updates, an `old` object is required. + if oldSv == nil { + errs = append(errs, field.InternalError(field.NewPath("spec"), errors.New("old object is nil"))) + + return errs + } + + // Only validate if one of the fields is being changed/set. + if sv.Spec.Value != "" || sv.Spec.Ref != "" { + if oldSv.Spec.Ref != "" && sv.Spec.Value != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set")) + } + + if oldSv.Spec.Ref == "" && sv.Spec.Ref != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `ref` when `value` was already previously set")) + } + } + + // Keeper cannot be changed. + if sv.Spec.Keeper != oldSv.Spec.Keeper { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "the `keeper` cannot be changed")) + } + + return errs +} + +// validateDecrypters validates that (if populated) the `decrypters` must match "actor_{name}" and must be unique. +func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList { + errs := make(field.ErrorList, 0) + + decrypterNames := make(map[string]struct{}, 0) + + for i, decrypter := range decrypters { + // Allow List: decrypters must match exactly and be in the allowed list to be able to decrypt. + // This means an allow list item should have the format "actor_{name}" and not just "{name}". + if len(decryptersAllowList) > 0 { + if _, exists := decryptersAllowList[decrypter]; !exists { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, fmt.Sprintf("allowed values: %v", decryptersAllowList)), + ) + + return errs + } + + continue + } + + actor, name, found := strings.Cut(strings.TrimSpace(decrypter), "_") + if !found || actor != "actor" || name == "" { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "a decrypter must have the format `actor_{name}`"), + ) + + continue + } + + if _, exists := decrypterNames[name]; exists { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"), + ) + + continue + } + + decrypterNames[name] = struct{}{} + } + + return errs +} diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go new file mode 100644 index 00000000000..3002be94706 --- /dev/null +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go @@ -0,0 +1,269 @@ +package reststorage + +import ( + "fmt" + "maps" + "slices" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/admission" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" +) + +func TestValidateSecureValue(t *testing.T) { + t.Run("when creating a new securevalue", func(t *testing.T) { + validSecureValue := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", + Value: "value", + Keeper: "keeper", + Decrypters: []string{"actor_app1", "actor_app2"}, + }, + } + + t.Run("the `title` must be present", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Title = "" + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec.title", errs[0].Field) + }) + + t.Run("the `keeper` must be present", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Keeper = "" + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec.keeper", errs[0].Field) + }) + + t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Value = "" + sv.Spec.Ref = "" + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + + sv.Spec.Value = "value" + sv.Spec.Ref = "value" + + errs = ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + }) + + t.Run("when updating a securevalue", func(t *testing.T) { + t.Run("when trying to switch from a `value` (old) to a `ref` (new), it returns an error", func(t *testing.T) { + oldSv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Ref: "", // empty `ref` means a `value` was present. + }, + } + + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Ref: "ref", + }, + } + + errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when trying to switch from a `ref` (old) to a `value` (new), it returns an error", func(t *testing.T) { + oldSv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Ref: "non-empty", + }, + } + + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Value: "value", + }, + } + + errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) { + oldSv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Ref: "non-empty", + }, + } + + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Value: "value", + Ref: "ref", + }, + } + + errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + + oldSv = &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Value: "non-empty", + }, + } + + errs = ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when no changes are made, it returns no errors", func(t *testing.T) { + oldSv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "old-title", + }, + } + + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "new-title", + }, + } + + errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Empty(t, errs) + }) + + t.Run("when the old object is `nil` it returns an error", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{} + + errs := ValidateSecureValue(sv, nil, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) { + oldSv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Keeper: "a-keeper", + }, + } + + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Keeper: "another-keeper", + }, + } + + errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + }) + + t.Run("`decrypters` must have unique items", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: []string{ + "actor_app1", + "actor_app1", + }, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec.decrypters.[1]", errs[0].Field) + }) + + t.Run("`decrypters` must match the expected format", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: []string{ + "app1", + "_app1", + "actr_app1", + "actor_ ", + "actor_", + }, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, len(sv.Spec.Decrypters)) + + for i, err := range errs { + require.Equal(t, fmt.Sprintf("spec.decrypters.[%d]", i), err.Field) + require.Contains(t, err.Error(), "a decrypter must have the format `actor_{name}`") + } + }) + + t.Run("when set, the `decrypters` must be one of the allowed in the allow list", func(t *testing.T) { + allowList := map[string]struct{}{"actor_app1": {}, "actor_app2": {}} + decrypters := slices.Collect(maps.Keys(allowList)) + + t.Run("no matches, returns an error", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: []string{"actor_app3"}, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, allowList) + require.Len(t, errs, 1) + }) + + t.Run("no decrypters, returns no error", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: []string{}, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, allowList) + require.Empty(t, errs) + }) + + t.Run("one match, returns no errors", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: []string{decrypters[0]}, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, allowList) + require.Empty(t, errs) + }) + + t.Run("all matches, returns no errors", func(t *testing.T) { + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Title: "title", Keeper: "keeper", Ref: "ref", + + Decrypters: decrypters, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, allowList) + require.Empty(t, errs) + }) + }) +} diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go new file mode 100644 index 00000000000..daf150d1543 --- /dev/null +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go @@ -0,0 +1,45 @@ +package secretkeeper + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" +) + +// Service is the interface for secret keeper services. +// This exists because OSS and Enterprise have different amounts of keepers available. +type Service interface { + GetKeepers() (map[contracts.KeeperType]contracts.Keeper, error) +} + +// OSSKeeperService is the OSS implementation of the Service interface. +type OSSKeeperService struct { + tracer tracing.Tracer + encryptionManager contracts.EncryptionManager + store contracts.EncryptedValueStorage +} + +func ProvideService( + tracer tracing.Tracer, + store contracts.EncryptedValueStorage, + encryptionManager contracts.EncryptionManager, +) (OSSKeeperService, error) { + return OSSKeeperService{ + tracer: tracer, + encryptionManager: encryptionManager, + store: store, + }, nil +} + +func (ks OSSKeeperService) GetKeepers() (map[contracts.KeeperType]contracts.Keeper, error) { + sqlKeeper, err := sqlkeeper.NewSQLKeeper(ks.tracer, ks.encryptionManager, ks.store) + if err != nil { + return nil, fmt.Errorf("failed to create sql keeper: %w", err) + } + + return map[contracts.KeeperType]contracts.Keeper{ + contracts.SQLKeeperType: sqlKeeper, + }, nil +} diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go new file mode 100644 index 00000000000..8a89eefc226 --- /dev/null +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -0,0 +1,35 @@ +package secretkeeper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/setting" +) + +func Test_OSSKeeperService_GetKeepers(t *testing.T) { + cfg := setting.NewCfg() + keeperService, err := setupTestService(t, cfg) + require.NoError(t, err) + + t.Run("GetKeepers should return a map with a sql keeper", func(t *testing.T) { + keeperMap, err := keeperService.GetKeepers() + require.NoError(t, err) + + assert.NotNil(t, keeperMap) + assert.Equal(t, 1, len(keeperMap)) + assert.IsType(t, &sqlkeeper.SQLKeeper{}, keeperMap[contracts.SQLKeeperType]) + }) +} + +func setupTestService(t *testing.T, cfg *setting.Cfg) (OSSKeeperService, error) { + // Initialize the keeper service + keeperService, err := ProvideService(tracing.InitializeTracerForTest(), nil, nil) + + return keeperService, err +} diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go new file mode 100644 index 00000000000..7e35a2f6aa0 --- /dev/null +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -0,0 +1,92 @@ +package sqlkeeper + +import ( + "context" + "fmt" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type SQLKeeper struct { + tracer tracing.Tracer + encryptionManager contracts.EncryptionManager + store contracts.EncryptedValueStorage +} + +var _ contracts.Keeper = (*SQLKeeper)(nil) + +func NewSQLKeeper( + tracer tracing.Tracer, + encryptionManager contracts.EncryptionManager, + store contracts.EncryptedValueStorage, +) (*SQLKeeper, error) { + return &SQLKeeper{ + tracer: tracer, + encryptionManager: encryptionManager, + store: store, + }, nil +} + +func (s *SQLKeeper) Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) { + ctx, span := s.tracer.Start(ctx, "sqlKeeper.Store") + defer span.End() + + encryptedData, err := s.encryptionManager.Encrypt(ctx, namespace, []byte(exposedValueOrRef), contracts.EncryptWithoutScope()) + if err != nil { + return "", fmt.Errorf("unable to encrypt value: %w", err) + } + + encryptedVal, err := s.store.Create(ctx, namespace, encryptedData) + if err != nil { + return "", fmt.Errorf("unable to store encrypted value: %w", err) + } + + return contracts.ExternalID(encryptedVal.UID), nil +} + +func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) (secretv0alpha1.ExposedSecureValue, error) { + ctx, span := s.tracer.Start(ctx, "sqlKeeper.Expose") + defer span.End() + + encryptedValue, err := s.store.Get(ctx, namespace, externalID.String()) + if err != nil { + return "", fmt.Errorf("unable to get encrypted value: %w", err) + } + + exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedData) + if err != nil { + return "", fmt.Errorf("unable to decrypt value: %w", err) + } + + exposedValue := secretv0alpha1.NewExposedSecureValue(string(exposedBytes)) + return exposedValue, nil +} + +func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) error { + ctx, span := s.tracer.Start(ctx, "sqlKeeper.Delete") + defer span.End() + + err := s.store.Delete(ctx, namespace, externalID.String()) + if err != nil { + return fmt.Errorf("failed to delete encrypted value: %w", err) + } + return nil +} + +func (s *SQLKeeper) Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID, exposedValueOrRef string) error { + ctx, span := s.tracer.Start(ctx, "sqlKeeper.Update") + defer span.End() + + encryptedData, err := s.encryptionManager.Encrypt(ctx, namespace, []byte(exposedValueOrRef), contracts.EncryptWithoutScope()) + if err != nil { + return fmt.Errorf("unable to encrypt value: %w", err) + } + + err = s.store.Update(ctx, namespace, externalID.String(), encryptedData) + if err != nil { + return fmt.Errorf("failed to update encrypted value: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go new file mode 100644 index 00000000000..a304c6cdc4c --- /dev/null +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -0,0 +1,237 @@ +package sqlkeeper + +import ( + "context" + "encoding/base64" + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/setting" +) + +// Make this a `TestIntegration` once we have the real storage implementation +func Test_SQLKeeperSetup(t *testing.T) { + ctx := context.Background() + namespace1 := "namespace1" + namespace2 := "namespace2" + plaintext1 := "very secret string in namespace 1" + plaintext2 := "very secret string in namespace 2" + nonExistentID := contracts.ExternalID("non existent") + + cfg := setting.NewCfg() + + sqlKeeper, err := setupTestService(t, cfg) + require.NoError(t, err) + require.NotNil(t, sqlKeeper) + + t.Run("storing an encrypted value returns no error", func(t *testing.T) { + externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId1) + + externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext2) + require.NoError(t, err) + require.NotEmpty(t, externalId2) + + t.Run("expose the encrypted value from existing namespace", func(t *testing.T) { + exposedVal1, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1) + require.NoError(t, err) + require.NotNil(t, exposedVal1) + assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue()) + + exposedVal2, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId2) + require.NoError(t, err) + require.NotNil(t, exposedVal2) + assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue()) + }) + + t.Run("expose encrypted value from different namespace returns error", func(t *testing.T) { + exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace2, externalId1) + require.Error(t, err) + assert.Empty(t, exposedVal) + + exposedVal, err = sqlKeeper.Expose(ctx, nil, namespace1, externalId2) + require.Error(t, err) + assert.Empty(t, exposedVal) + }) + }) + + t.Run("storing same value in same namespace returns no error", func(t *testing.T) { + externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId1) + + externalId2, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId2) + + assert.NotEqual(t, externalId1, externalId2) + }) + + t.Run("storing same value in different namespace returns no error", func(t *testing.T) { + externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId1) + + externalId2, err := sqlKeeper.Store(ctx, nil, namespace2, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId2) + + assert.NotEqual(t, externalId1, externalId2) + }) + + t.Run("exposing non existing values returns error", func(t *testing.T) { + exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, nonExistentID) + require.Error(t, err) + assert.Empty(t, exposedVal) + }) + + t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) { + externalID, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalID) + + exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalID) + require.NoError(t, err) + assert.NotNil(t, exposedVal) + assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue()) + + err = sqlKeeper.Delete(ctx, nil, namespace1, externalID) + require.NoError(t, err) + }) + + t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) { + err = sqlKeeper.Delete(ctx, nil, namespace1, nonExistentID) + require.NoError(t, err) + }) + + t.Run("updating an existent encrypted value returns no error", func(t *testing.T) { + externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId1) + + err = sqlKeeper.Update(ctx, nil, namespace1, externalId1, plaintext2) + require.NoError(t, err) + + exposedVal, err := sqlKeeper.Expose(ctx, nil, namespace1, externalId1) + require.NoError(t, err) + assert.NotNil(t, exposedVal) + assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue()) + }) + + t.Run("updating a non existent encrypted value returns error", func(t *testing.T) { + externalId1, err := sqlKeeper.Store(ctx, nil, namespace1, plaintext1) + require.NoError(t, err) + require.NotEmpty(t, externalId1) + + err = sqlKeeper.Update(ctx, nil, namespace1, nonExistentID, plaintext2) + require.Error(t, err) + }) +} + +func setupTestService(t *testing.T, cfg *setting.Cfg) (*SQLKeeper, error) { + // Initialize the encryption manager with in-memory implementation + encMgr := &inMemoryEncryptionManager{} + + // Initialize encrypted value storage with in-memory implementation + encValueStore := newInMemoryEncryptedValueStorage() + + // Initialize the SQLKeeper + sqlKeeper, err := NewSQLKeeper(tracing.InitializeTracerForTest(), encMgr, encValueStore) + + return sqlKeeper, err +} + +// While we don't have the real implementation, use an in-memory one +type inMemoryEncryptionManager struct{} + +func (m *inMemoryEncryptionManager) Encrypt(_ context.Context, _ string, value []byte, _ contracts.EncryptionOptions) ([]byte, error) { + return []byte(base64.StdEncoding.EncodeToString(value)), nil +} + +func (m *inMemoryEncryptionManager) Decrypt(_ context.Context, _ string, value []byte) ([]byte, error) { + return base64.StdEncoding.DecodeString(string(value)) +} + +func (m *inMemoryEncryptionManager) ReEncryptDataKeys(_ context.Context, _ string) error { + return nil +} + +func (m *inMemoryEncryptionManager) RotateDataKeys(_ context.Context, _ string) error { + return nil +} + +// While we don't have the real implementation, use an in-memory one +type inMemoryEncryptedValueStorage struct { + mu sync.RWMutex + store map[string]*contracts.EncryptedValue +} + +func newInMemoryEncryptedValueStorage() *inMemoryEncryptedValueStorage { + return &inMemoryEncryptedValueStorage{ + store: make(map[string]*contracts.EncryptedValue), + } +} + +func (m *inMemoryEncryptedValueStorage) Create(_ context.Context, namespace string, encryptedData []byte) (*contracts.EncryptedValue, error) { + m.mu.Lock() + defer m.mu.Unlock() + + uid := fmt.Sprintf("%d", len(m.store)+1) // Generate simple incremental IDs + encValue := &contracts.EncryptedValue{ + UID: uid, + Namespace: namespace, + EncryptedData: encryptedData, + Created: 1, // Dummy timestamp + Updated: 1, // Dummy timestamp + } + + compositeKey := namespace + ":" + uid + m.store[compositeKey] = encValue + + return encValue, nil +} + +func (m *inMemoryEncryptedValueStorage) Get(_ context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + compositeKey := namespace + ":" + uid + encValue, exists := m.store[compositeKey] + if !exists { + return nil, fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid) + } + + return encValue, nil +} + +func (m *inMemoryEncryptedValueStorage) Delete(_ context.Context, namespace string, uid string) error { + m.mu.Lock() + defer m.mu.Unlock() + + compositeKey := namespace + ":" + uid + delete(m.store, compositeKey) + + return nil +} + +func (m *inMemoryEncryptedValueStorage) Update(_ context.Context, namespace string, uid string, encryptedData []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + compositeKey := namespace + ":" + uid + encValue, exists := m.store[compositeKey] + if !exists { + return fmt.Errorf("value not found for namespace %s and uid %s", namespace, uid) + } + + encValue.EncryptedData = encryptedData + encValue.Updated = 2 // Update timestamp + return nil +} diff --git a/pkg/registry/apis/secret/xkube/annotations.go b/pkg/registry/apis/secret/xkube/annotations.go new file mode 100644 index 00000000000..21e7cbbac74 --- /dev/null +++ b/pkg/registry/apis/secret/xkube/annotations.go @@ -0,0 +1,24 @@ +package xkube + +import "github.com/grafana/grafana/pkg/apimachinery/utils" + +var ( + // Exclude these annotations + skipAnnotations = map[string]bool{ + "kubectl.kubernetes.io/last-applied-configuration": true, // force server side apply + utils.AnnoKeyCreatedBy: true, + utils.AnnoKeyUpdatedBy: true, + utils.AnnoKeyUpdatedTimestamp: true, + } +) + +func CleanAnnotations(anno map[string]string) map[string]string { + copy := make(map[string]string) + for k, v := range anno { + if skipAnnotations[k] { + continue + } + copy[k] = v + } + return copy +} diff --git a/pkg/registry/apis/secret/xkube/errors.go b/pkg/registry/apis/secret/xkube/errors.go new file mode 100644 index 00000000000..3de10eeffcf --- /dev/null +++ b/pkg/registry/apis/secret/xkube/errors.go @@ -0,0 +1,9 @@ +package xkube + +import "k8s.io/apimachinery/pkg/util/validation/field" + +// ErrorLister is an interface compatible with errors that also returns a list of Kubernetes field errors. +type ErrorLister interface { + error + ErrorList() field.ErrorList +} diff --git a/pkg/registry/apis/secret/xkube/namespace.go b/pkg/registry/apis/secret/xkube/namespace.go new file mode 100644 index 00000000000..72fb3f3afd2 --- /dev/null +++ b/pkg/registry/apis/secret/xkube/namespace.go @@ -0,0 +1,8 @@ +package xkube + +// Namespace is a newtype of string that improves type safety. +type Namespace string + +func (n Namespace) String() string { + return string(n) +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index ecd0cba0afb..3ff07823e1a 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/service" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -36,5 +37,6 @@ var WireSet = wire.NewSet( service.RegisterAPIService, query.RegisterAPIService, notifications.RegisterAPIService, + secret.RegisterAPIService, userstorage.RegisterAPIService, ) diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index b0f339ed29d..621c2eee751 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authnimpl" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/services/guardian" @@ -69,6 +70,7 @@ func ProvideBackgroundServiceRegistry( zanzanaReconciler *dualwrite.ZanzanaReconciler, appRegistry *appregistry.Service, pluginDashboardUpdater *plugindashboardsservice.DashboardUpdater, + dashboardServiceImpl *service.DashboardServiceImpl, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ serviceaccounts.Service, _ *guardian.Provider, @@ -115,6 +117,7 @@ func ProvideBackgroundServiceRegistry( zanzanaReconciler, appRegistry, pluginDashboardUpdater, + dashboardServiceImpl, ) } diff --git a/pkg/semconv/attributes.go b/pkg/semconv/attributes.go index c05fd9f4e5b..07fc5fa6fd6 100644 --- a/pkg/semconv/attributes.go +++ b/pkg/semconv/attributes.go @@ -4,25 +4,6 @@ package semconv import "go.opentelemetry.io/otel/attribute" -// Describes Grafana service attributes. -const ( - // GrafanaServiceNameKey is the attribute Key conforming to the - // "grafana.service.name" semantic conventions. It represents the service - // name. - // - // Type: string - // RequirementLevel: Optional - // Stability: stable - // Examples: 'grafana-apiserver' - grafanaServiceNameKey = attribute.Key("grafana.service.name") -) - -// GrafanaServiceName returns an attribute KeyValue conforming to the -// "grafana.service.name" semantic conventions. It represents the service name. -func GrafanaServiceName(val string) attribute.KeyValue { - return grafanaServiceNameKey.String(val) -} - // Describes Grafana datasource attributes. const ( // GrafanaDatasourceTypeKey is the attribute Key conforming to the @@ -131,8 +112,6 @@ var ( GrafanaPluginTypeApp = grafanaPluginTypeKey.String("app") // Renderer Plugin GrafanaPluginTypeRenderer = grafanaPluginTypeKey.String("renderer") - // Secret Manager Plugin - GrafanaPluginTypeSecretmanager = grafanaPluginTypeKey.String("secretmanager") ) // GrafanaPluginId returns an attribute KeyValue conforming to the @@ -140,3 +119,22 @@ var ( func GrafanaPluginId(val string) attribute.KeyValue { return grafanaPluginIdKey.String(val) } + +// Describes Grafana service attributes. +const ( + // GrafanaServiceNameKey is the attribute Key conforming to the + // "grafana.service.name" semantic conventions. It represents the service + // name. + // + // Type: string + // RequirementLevel: Optional + // Stability: stable + // Examples: 'grafana-apiserver' + grafanaServiceNameKey = attribute.Key("grafana.service.name") +) + +// GrafanaServiceName returns an attribute KeyValue conforming to the +// "grafana.service.name" semantic conventions. It represents the service name. +func GrafanaServiceName(val string) attribute.KeyValue { + return grafanaServiceNameKey.String(val) +} diff --git a/pkg/semconv/model/registry/plugin.yml b/pkg/semconv/model/registry/plugin.yml index 89f088fe2cb..431794bcd34 100644 --- a/pkg/semconv/model/registry/plugin.yml +++ b/pkg/semconv/model/registry/plugin.yml @@ -31,10 +31,6 @@ groups: value: "renderer" brief: 'Renderer Plugin' stability: stable - - id: secretmanager - value: "secretmanager" - brief: 'Secret Manager Plugin' - stability: stable brief: The plugin type. examples: datasource - stability: stable \ No newline at end of file + stability: stable diff --git a/pkg/server/instrumentation_service.go b/pkg/server/instrumentation_service.go index 7eca2a54fda..9a200ec7f38 100644 --- a/pkg/server/instrumentation_service.go +++ b/pkg/server/instrumentation_service.go @@ -9,19 +9,21 @@ import ( "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) type instrumentationService struct { *services.BasicService - cfg *setting.Cfg - httpServ *http.Server - log log.Logger - errChan chan error + cfg *setting.Cfg + httpServ *http.Server + log log.Logger + errChan chan error + promGatherer prometheus.Gatherer } -func NewInstrumentationService(log log.Logger, cfg *setting.Cfg) (*instrumentationService, error) { - s := &instrumentationService{log: log, cfg: cfg} +func NewInstrumentationService(log log.Logger, cfg *setting.Cfg, promGatherer prometheus.Gatherer) (*instrumentationService, error) { + s := &instrumentationService{log: log, cfg: cfg, promGatherer: promGatherer} s.BasicService = services.NewBasicService(s.start, s.running, s.stop) return s, nil } @@ -56,7 +58,7 @@ func (s *instrumentationService) stop(failureReason error) error { func (s *instrumentationService) newInstrumentationServer(ctx context.Context) *http.Server { router := http.NewServeMux() - router.Handle("/metrics", promhttp.Handler()) + router.Handle("/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true})) srv := &http.Server{ // 5s timeout for header reads to avoid Slowloris attacks (https://thetooth.io/blog/slowloris-attack/) diff --git a/pkg/server/instrumentation_service_test.go b/pkg/server/instrumentation_service_test.go index 51af5ee4032..1dc8ffa8349 100644 --- a/pkg/server/instrumentation_service_test.go +++ b/pkg/server/instrumentation_service_test.go @@ -18,7 +18,7 @@ import ( func TestRunInstrumentationService(t *testing.T) { cfg := setting.NewCfg() cfg.HTTPPort = "3001" - s, err := NewInstrumentationService(log.New("test-logger"), cfg) + s, err := NewInstrumentationService(log.New("test-logger"), cfg, prometheus.DefaultGatherer) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 82fd314f5e1..11db7c204d3 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -18,12 +18,13 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/prometheus/client_golang/prometheus" ) // NewModule returns an instance of a ModuleServer, responsible for managing // dskit modules (services). -func NewModule(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (*ModuleServer, error) { - s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics) +func NewModule(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, promGatherer prometheus.Gatherer) (*ModuleServer, error) { + s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, promGatherer) if err != nil { return nil, err } @@ -35,7 +36,7 @@ func NewModule(opts Options, apiOpts api.ServerOptions, features featuremgmt.Fea return s, nil } -func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (*ModuleServer, error) { +func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, promGatherer prometheus.Gatherer) (*ModuleServer, error) { rootCtx, shutdownFn := context.WithCancel(context.Background()) s := &ModuleServer{ @@ -53,6 +54,7 @@ func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremg buildBranch: opts.BuildBranch, storageMetrics: storageMetrics, indexMetrics: indexMetrics, + promGatherer: promGatherer, } return s, nil @@ -81,6 +83,8 @@ type ModuleServer struct { version string commit string buildBranch string + + promGatherer prometheus.Gatherer } // init initializes the server and its services. @@ -119,7 +123,7 @@ func (s *ModuleServer) Run() error { if m.IsModuleEnabled(modules.All) || m.IsModuleEnabled(modules.Core) { return services.NewBasicService(nil, nil, nil).WithName(modules.InstrumentationServer), nil } - return NewInstrumentationService(s.log, s.cfg) + return NewInstrumentationService(s.log, s.cfg, s.promGatherer) }) m.RegisterModule(modules.Core, func() (services.Service, error) { diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index f60f9c6703e..fdf52796a8a 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" @@ -12,9 +13,14 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/web" + "github.com/stretchr/testify/mock" ) func ProvideTestEnv( + testingT interface { + mock.TestingT + Cleanup(func()) + }, server *Server, db db.DB, cfg *setting.Cfg, @@ -26,8 +32,10 @@ func ProvideTestEnv( featureMgmt featuremgmt.FeatureToggles, resourceClient resource.ResourceClient, idService auth.IDService, + githubFactory *github.Factory, ) (*TestEnv, error) { return &TestEnv{ + TestingT: testingT, Server: server, SQLStore: db, Cfg: cfg, @@ -39,10 +47,15 @@ func ProvideTestEnv( FeatureToggles: featureMgmt, ResourceClient: resourceClient, IDService: idService, + GitHubFactory: githubFactory, }, nil } type TestEnv struct { + TestingT interface { + mock.TestingT + Cleanup(func()) + } Server *Server SQLStore db.DB Cfg *setting.Cfg @@ -55,4 +68,5 @@ type TestEnv struct { FeatureToggles featuremgmt.FeatureToggles ResourceClient resource.ResourceClient IDService auth.IDService + GitHubFactory *github.Factory } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index e211b739ecb..993c580171a 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -8,6 +8,7 @@ package server import ( "github.com/google/wire" + "github.com/stretchr/testify/mock" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -37,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/middleware/loggermw" apiregistry "github.com/grafana/grafana/pkg/registry/apis" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -159,6 +161,7 @@ import ( "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" legacydualwrite "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + secretmetadata "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/unified/resource" unifiedsearch "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -258,6 +261,7 @@ var wireBasicSet = wire.NewSet( 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)), @@ -362,8 +366,6 @@ var wireBasicSet = wire.NewSet( loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), secretsMigrations.ProvideDataSourceMigrationService, - secretsMigrations.ProvideMigrateToPluginService, - secretsMigrations.ProvideMigrateFromPluginService, secretsMigrations.ProvideSecretMigrationProvider, wire.Bind(new(secretsMigrations.SecretMigrationProvider), new(*secretsMigrations.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, @@ -402,6 +404,9 @@ var wireBasicSet = wire.NewSet( connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, + // Secrets Manager + secretmetadata.ProvideSecureValueMetadataStorage, + secretmetadata.ProvideKeeperMetadataStorage, // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, @@ -464,9 +469,12 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return &Server{}, nil } -func InitializeForTest(t sqlutil.ITestDB, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { +func InitializeForTest(t sqlutil.ITestDB, testingT interface { + mock.TestingT + Cleanup(func()) +}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { wire.Build(wireExtsTestSet) - return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil + return &TestEnv{Server: &Server{}, TestingT: testingT, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil } func InitializeForCLI(cfg *setting.Cfg) (Runner, error) { diff --git a/pkg/services/accesscontrol/permreg/permreg.go b/pkg/services/accesscontrol/permreg/permreg.go index 361b2600d08..1d46d749b38 100644 --- a/pkg/services/accesscontrol/permreg/permreg.go +++ b/pkg/services/accesscontrol/permreg/permreg.go @@ -76,24 +76,26 @@ func ProvidePermissionRegistry() PermissionRegistry { func newPermissionRegistry() *permissionRegistry { // defaultKindScopes maps the most specific accepted scope prefix for a given kind (folders, dashboards, etc) defaultKindScopes := map[string]string{ - "teams": "teams:id:", - "users": "users:id:", - "datasources": "datasources:uid:", - "dashboards": "dashboards:uid:", - "folders": "folders:uid:", - "annotations": "annotations:type:", - "apikeys": "apikeys:id:", - "orgs": "orgs:id:", - "plugins": "plugins:id:", - "provisioners": "provisioners:", - "reports": "reports:id:", - "permissions": "permissions:type:", - "serviceaccounts": "serviceaccounts:id:", - "settings": "settings:", - "global.users": "global.users:id:", - "roles": "roles:uid:", - "services": "services:", - "receivers": "receivers:uid:", + "teams": "teams:id:", + "users": "users:id:", + "datasources": "datasources:uid:", + "dashboards": "dashboards:uid:", + "folders": "folders:uid:", + "annotations": "annotations:type:", + "apikeys": "apikeys:id:", + "orgs": "orgs:id:", + "plugins": "plugins:id:", + "provisioners": "provisioners:", + "reports": "reports:id:", + "permissions": "permissions:type:", + "serviceaccounts": "serviceaccounts:id:", + "settings": "settings:", + "global.users": "global.users:id:", + "roles": "roles:uid:", + "services": "services:", + "receivers": "receivers:uid:", + "secret.securevalues": "secret.securevalues:uid:", + "secret.keepers": "secret.keepers:uid:", } return &permissionRegistry{ actionScopePrefixes: make(map[string]PrefixSet, 200), diff --git a/pkg/services/accesscontrol/resourcepermissions/store.go b/pkg/services/accesscontrol/resourcepermissions/store.go index c8d8e8db88e..f7444376ee9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store.go +++ b/pkg/services/accesscontrol/resourcepermissions/store.go @@ -449,7 +449,7 @@ func (s *store) getResourcePermissions(sess *db.Session, orgID int64, query GetR builtin := builtinSelect + builtinFrom + where args = append(args, args[:initialLength]...) - sql := userQuery + " UNION " + team + " UNION " + builtin + sql := userQuery + " " + s.sql.GetDialect().UnionDistinct() + " " + team + " " + s.sql.GetDialect().UnionDistinct() + " " + builtin queryResults := make([]flatResourcePermission, 0) if err := sess.SQL(sql, args...).Find(&queryResults); err != nil { return nil, err diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 3dc79fe876f..c92e1b9546b 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -10,9 +10,11 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" @@ -22,7 +24,6 @@ import ( dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" @@ -42,19 +43,18 @@ func TestIntegrationAuthorize(t *testing.T) { } sql, cfg := db.InitTestDBWithCfg(t) - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) folderStore := folderimpl.ProvideDashboardFolderStore(sql) fStore := folderimpl.ProvideStore(sql) dashStore, err := database.ProvideDashboardStore(sql, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql)) require.NoError(t, err) - ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderSvc := folderimpl.ProvideService( - fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, + fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sql, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 0615023b430..8e169e88401 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -12,10 +12,12 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" @@ -54,19 +56,18 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { features := featuremgmt.WithFeatures() tagService := tagimpl.ProvideService(sql) ruleStore := alertingStore.SetupStoreForTesting(t, sql) - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{}) folderStore := folderimpl.ProvideDashboardFolderStore(sql) fStore := folderimpl.ProvideStore(sql) dashStore, err := database.ProvideDashboardStore(sql, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql)) require.NoError(t, err) - ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderSvc := folderimpl.ProvideService( - fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, + fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sql, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc) @@ -242,14 +243,17 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { guardian.New = origNewGuardian }) - ac := acimpl.ProvideAccessControl(features) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} fStore := folderimpl.ProvideStore(sql) folderStore := folderimpl.ProvideDashboardFolderStore(sql) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sql, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), + ) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) cfg.AnnotationMaximumTagsLength = 60 diff --git a/pkg/services/annotations/annotationsimpl/cleanup_test.go b/pkg/services/annotations/annotationsimpl/cleanup_test.go index 278bb48e365..324699736b1 100644 --- a/pkg/services/annotations/annotationsimpl/cleanup_test.go +++ b/pkg/services/annotations/annotationsimpl/cleanup_test.go @@ -119,8 +119,8 @@ func TestIntegrationAnnotationCleanUp(t *testing.T) { t.Cleanup(func() { err := fakeSQL.WithDbSession(context.Background(), func(session *db.Session) error { - _, deleteAnnotationErr := session.Exec("DELETE FROM annotation") - _, deleteAnnotationTagErr := session.Exec("DELETE FROM annotation_tag") + _, deleteAnnotationErr := session.Exec("DELETE FROM annotation WHERE true") + _, deleteAnnotationTagErr := session.Exec("DELETE FROM annotation_tag WHERE true") return errors.Join(deleteAnnotationErr, deleteAnnotationTagErr) }) assert.NoError(t, err) @@ -157,7 +157,7 @@ func TestIntegrationOldAnnotationsAreDeletedFirst(t *testing.T) { t.Cleanup(func() { err := fakeSQL.WithDbSession(context.Background(), func(session *db.Session) error { - _, err := session.Exec("DELETE FROM annotation") + _, err := session.Exec("DELETE FROM annotation WHERE true") return err }) assert.NoError(t, err) diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index 7b957189312..8bfa69e80cd 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -333,10 +333,11 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer } if len(tags) > 0 { + // "at" is a keyword in Spanner and needs to be quoted. tagsSubQuery := fmt.Sprintf(` - SELECT SUM(1) FROM annotation_tag at - INNER JOIN tag on tag.id = at.tag_id - WHERE at.annotation_id = a.id + SELECT SUM(1) FROM annotation_tag `+r.db.Quote("at")+` + INNER JOIN tag on tag.id = `+r.db.Quote("at")+`.tag_id + WHERE `+r.db.Quote("at")+`.annotation_id = a.id AND ( %s ) diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index cb6d2626061..4152f084b07 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -170,6 +170,8 @@ func SetupConfig( operationAlt = "get" + operationAlt[len("read"):] } else if strings.HasPrefix(operationAlt, "patch") { operationAlt = "update" + operationAlt[len("patch"):] + } else if strings.HasPrefix(operationAlt, "put") { + operationAlt = "replace" + operationAlt[len("put"):] } // Audit our options here diff --git a/pkg/services/apiserver/client/discovery.go b/pkg/services/apiserver/client/discovery.go index bdf2a46a29e..3c9b91f1ab4 100644 --- a/pkg/services/apiserver/client/discovery.go +++ b/pkg/services/apiserver/client/discovery.go @@ -2,6 +2,7 @@ package client import ( "fmt" + "strings" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" @@ -12,6 +13,7 @@ type DiscoveryClient interface { discovery.DiscoveryInterface GetResourceForKind(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) GetKindForResource(gvr schema.GroupVersionResource) (schema.GroupVersionKind, error) + GetPreferredVesion(gr schema.GroupResource) (schema.GroupVersionResource, schema.GroupVersionKind, error) } type DiscoveryClientImpl struct { @@ -63,3 +65,30 @@ func (d *DiscoveryClientImpl) GetKindForResource(gvr schema.GroupVersionResource } return schema.GroupVersionKind{}, fmt.Errorf("kind not found for %s", gvr.String()) } + +func (d *DiscoveryClientImpl) GetPreferredVesion(gr schema.GroupResource) (schema.GroupVersionResource, schema.GroupVersionKind, error) { + apiList, err := d.ServerPreferredResources() + if err != nil { + return schema.GroupVersionResource{}, schema.GroupVersionKind{}, err + } + for _, apis := range apiList { + if !strings.HasPrefix(apis.GroupVersion, gr.Group) { + continue + } + gv := strings.Split(apis.GroupVersion, "/") + for _, resource := range apis.APIResources { + if resource.Name == gr.Resource { + return schema.GroupVersionResource{ + Group: gv[0], + Version: gv[1], + Resource: resource.Name, + }, schema.GroupVersionKind{ + Group: gv[0], + Version: gv[1], + Kind: resource.Kind, + }, nil + } + } + } + return schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("preferred version not found for %s", gr.String()) +} diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index e37ac50fda9..f7c2be03cbc 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -87,6 +87,15 @@ func init() { Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...) } +// ClearRestConfig clears the package level restConfig. +// This is intended to be used in tests only. +// +// TODO: Refactor such that there is no global state. +func ClearRestConfig() { + restConfig = nil + ready = make(chan struct{}) +} + // GetRestConfig return a client Config mounted at package level // This resolves circular dependency issues between apiserver, authz, // and Folder Service. diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index fecfc17bf3a..d7fab0a65b3 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -31,6 +31,7 @@ const ( ClientSAML = "auth.client.saml" ClientPasswordless = "auth.client.passwordless" ClientLDAP = "ldap" + ClientProvisioning = "auth.client.apiserver.provisioning" ) const ( diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index c5af37a3098..8143c4e755a 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/apikey" @@ -112,7 +113,8 @@ func ProvideRegistration( } if cfg.JWTAuth.Enabled { - authnSvc.RegisterClient(clients.ProvideJWT(jwtService, cfg)) + orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) + authnSvc.RegisterClient(clients.ProvideJWT(jwtService, orgRoleMapper, cfg)) } if cfg.ExtJWTAuth.Enabled { @@ -124,6 +126,10 @@ func ProvideRegistration( authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features)) } + if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { + authnSvc.RegisterClient(clients.ProvideProvisioning()) + } + // FIXME (jguer): move to User package userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer, features) orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer) @@ -142,6 +148,7 @@ func ProvideRegistration( authnSvc.RegisterPostAuthHook(rbacSync.SyncPermissionsHook, 120) authnSvc.RegisterPostLoginHook(orgSync.SetDefaultOrgHook, 140) + authnSvc.RegisterPostLoginHook(rbacSync.ClearUserPermissionCacheHook, 170) nsSync := sync.ProvideNamespaceSync(cfg) authnSvc.RegisterPostAuthHook(nsSync.SyncNamespace, 150) diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync.go b/pkg/services/authn/authnimpl/sync/rbac_sync.go index 8af08a38f51..75f6324382f 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync.go @@ -186,3 +186,22 @@ func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r RolesToRemove: rolesToRemove, }) } + +// ClearUserPermissionCacheHook clears a user's permission cache if user Login succeeded. Necessary so that if a user logs in +// through different SSO providers with different roles assigned in each, they do not get the wrong permissions. +func (s *RBACSync) ClearUserPermissionCacheHook(ctx context.Context, ident *authn.Identity, r *authn.Request, err error) { + ctx, span := s.tracer.Start(ctx, "rbac.sync.ClearUserPermissionCacheHook") + defer span.End() + + if err != nil { + return + } + + ctxLogger := s.log.FromContext(ctx) + if !ident.IsIdentityType(claims.TypeUser) { + ctxLogger.Debug("Skipping user permission cache clear, not a user", "type", ident.GetIdentityType()) + return + } + + s.ac.ClearUserPermissionCache(ident) +} diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go index 594e9d80a1c..97c27942f22 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go @@ -2,6 +2,7 @@ package sync import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -365,6 +366,61 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { } } +func TestRBACSync_ClearUserPermissionCacheHook(t *testing.T) { + type testCase struct { + desc string + identityType claims.IdentityType + loginErr error + expectedCalled bool + } + + tests := []testCase{ + { + desc: "should clear the permission cache when the user logged in successfully", + identityType: claims.TypeUser, + loginErr: nil, + expectedCalled: true, + }, + { + desc: "should skip clearing the permission cache when the user failed to log in", + identityType: claims.TypeUser, + loginErr: errors.New("failed to log in"), + expectedCalled: false, + }, + { + desc: "should skip clearing the permission cache when the identity is not a user", + identityType: claims.TypeServiceAccount, + loginErr: nil, + expectedCalled: false, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var called bool + s := &RBACSync{ + ac: &acmock.Mock{ + ClearUserPermissionCacheFunc: func(_ identity.Requester) { + called = true + }, + }, + log: log.NewNopLogger(), + tracer: tracing.InitializeTracerForTest(), + } + identity := &authn.Identity{ + ID: "1", + Type: tt.identityType, + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + } + req := &authn.Request{} + + s.ClearUserPermissionCacheHook(context.Background(), identity, req, tt.loginErr) + assert.Equal(t, tt.expectedCalled, called) + }) + } +} + func setupTestEnv(t *testing.T) *RBACSync { acMock := &acmock.Mock{ GetUserPermissionsFunc: func(ctx context.Context, siu identity.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) { diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index f1ccefaae48..2f55c3a9cac 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -7,6 +7,7 @@ import ( "strconv" claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/singleflight" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -45,6 +46,14 @@ var ( "user.sync.fetch-not-found", errutil.WithPublicMessage("User not found"), ) + errMismatchedExternalUID = errutil.Unauthorized( + "user.sync.mismatched-externalUID", + errutil.WithPublicMessage("Mismatched externalUID"), + ) + errEmptyExternalUID = errutil.Unauthorized( + "user.sync.empty-externalUID", + errutil.WithPublicMessage("Empty externalUID"), + ) ) var ( @@ -231,7 +240,7 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident return nil } - // If a user does not a connection to a specific auth module, create it. + // If a user does not have a connection to a specific auth module, create it. // This can happen when: using multiple auth client where the same user exists in several or // changing to new auth client if createConnection { @@ -265,6 +274,8 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id ctx, span := s.tracer.Start(ctx, "user.sync.updateUserAttributes") defer span.End() + needsConnectionCreation := userAuth == nil + if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil { return errUserProtection.Errorf("user mapping not allowed: %w", errProtection) } @@ -305,14 +316,38 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id needsUpdate = true } - if needsUpdate { + span.SetAttributes( + attribute.String("identity.ID", id.ID), + attribute.String("identity.ExternalUID", id.ExternalUID), + ) + if usr.IsProvisioned { + s.log.Debug("User is provisioned", "id,UID", id.UID) + needsConnectionCreation = false + authInfo, err := s.authInfoService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: id.AuthenticatedBy}) + if err != nil { + s.log.Error("Error getting auth info", "error", err) + return err + } + + if id.ExternalUID == "" { + s.log.Error("externalUID is empty", "id", id.UID) + return errEmptyExternalUID.Errorf("externalUID is empty") + } + + if id.ExternalUID != authInfo.ExternalUID { + s.log.Error("mismatched externalUID", "provisioned_externalUID", authInfo.ExternalUID, "identity_externalUID", id.ExternalUID) + return errMismatchedExternalUID.Errorf("externalUID mistmatch") + } + } + + if needsUpdate && !usr.IsProvisioned { s.log.FromContext(ctx).Debug("Syncing user info", "id", id.ID, "update", fmt.Sprintf("%v", updateCmd)) if err := s.userService.Update(ctx, updateCmd); err != nil { return err } } - return s.upsertAuthConnection(ctx, usr.ID, id, userAuth == nil) + return s.upsertAuthConnection(ctx, usr.ID, id, needsConnectionCreation) } func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) { diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index edc1f43dbc9..82323e21bfe 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/auth" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" @@ -29,18 +30,22 @@ var ( "jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim")) ) -func ProvideJWT(jwtService auth.JWTVerifierService, cfg *setting.Cfg) *JWT { +func ProvideJWT(jwtService auth.JWTVerifierService, orgRoleMapper *connectors.OrgRoleMapper, cfg *setting.Cfg) *JWT { return &JWT{ - cfg: cfg, - log: log.New(authn.ClientJWT), - jwtService: jwtService, + cfg: cfg, + log: log.New(authn.ClientJWT), + jwtService: jwtService, + orgRoleMapper: orgRoleMapper, + orgMappingCfg: orgRoleMapper.ParseOrgMappingSettings(context.Background(), cfg.JWTAuth.OrgMapping, cfg.JWTAuth.RoleAttributeStrict), } } type JWT struct { - cfg *setting.Cfg - log log.Logger - jwtService auth.JWTVerifierService + cfg *setting.Cfg + orgRoleMapper *connectors.OrgRoleMapper + orgMappingCfg connectors.MappingConfiguration + log log.Logger + jwtService auth.JWTVerifierService } func (s *JWT) Name() string { @@ -102,32 +107,31 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi id.Name = name } - orgRoles, isGrafanaAdmin, err := getRoles(s.cfg, func() (org.RoleType, *bool, error) { - if s.cfg.JWTAuth.SkipOrgRoleSync { - return "", nil, nil - } - - role, grafanaAdmin := s.extractRoleAndAdmin(claims) - if s.cfg.JWTAuth.RoleAttributeStrict && !role.IsValid() { - return "", nil, errJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role) - } - - if !s.cfg.JWTAuth.AllowAssignGrafanaAdmin { - return role, nil, nil - } - - return role, &grafanaAdmin, nil - }) + id.Groups, err = s.extractGroups(claims) if err != nil { return nil, err } - id.OrgRoles = orgRoles - id.IsGrafanaAdmin = isGrafanaAdmin + if !s.cfg.JWTAuth.SkipOrgRoleSync { + role, grafanaAdmin := s.extractRoleAndAdmin(claims) + if err != nil { + s.log.Warn("Failed to extract role", "err", err) + } - id.Groups, err = s.extractGroups(claims) - if err != nil { - return nil, err + if s.cfg.JWTAuth.AllowAssignGrafanaAdmin { + id.IsGrafanaAdmin = &grafanaAdmin + } + + externalOrgs, err := s.extractOrgs(claims) + if err != nil { + s.log.Warn("Failed to extract orgs", "err", err) + return nil, err + } + + id.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, externalOrgs, role) + if s.cfg.JWTAuth.RoleAttributeStrict && len(id.OrgRoles) == 0 { + return nil, errJWTInvalidRole.Errorf("could not evaluate any valid roles using IdP provided data") + } } if id.Login == "" && id.Email == "" { @@ -213,3 +217,12 @@ func (s *JWT) extractGroups(claims map[string]any) ([]string, error) { return util.SearchJSONForStringSliceAttr(s.cfg.JWTAuth.GroupsAttributePath, claims) } + +// This code was copied from the social_base.go file and was adapted to match with the JWT structure +func (s *JWT) extractOrgs(claims map[string]any) ([]string, error) { + if s.cfg.JWTAuth.OrgAttributePath == "" { + return []string{}, nil + } + + return util.SearchJSONForStringSliceAttr(s.cfg.JWTAuth.OrgAttributePath, claims) +} diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index c4381f3945e..7fccffc6b17 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -11,9 +11,12 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -136,6 +139,116 @@ func TestAuthenticateJWT(t *testing.T) { }, }, }, + { + name: "Valid Use case with org_mapping", + wantID: &authn.Identity{ + OrgID: 0, + OrgName: "", + OrgRoles: map[int64]identity.RoleType{4: identity.RoleEditor, 5: identity.RoleViewer}, + Login: "eai-doe", + Groups: []string{"foo", "bar"}, + Name: "Eai Doe", + Email: "eai.doe@cor.po", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.JWTModule, + AuthID: "1234567890", + IsDisabled: false, + HelpFlags1: 0, + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + FetchSyncedUser: true, + SyncOrgRoles: true, + SyncPermissions: true, + SyncTeams: true, + LookUpParams: login.UserLookupParams{ + Email: stringPtr("eai.doe@cor.po"), + Login: stringPtr("eai-doe"), + }, + }, + }, + verifyProvider: func(context.Context, string) (map[string]any, error) { + return map[string]any{ + "sub": "1234567890", + "email": "eai.doe@cor.po", + "preferred_username": "eai-doe", + "name": "Eai Doe", + "roles": "None", + "groups": []string{"foo", "bar"}, + "orgs": []string{"org1", "org2"}, + }, nil + }, + cfg: &setting.Cfg{ + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + EmailClaim: "email", + UsernameClaim: "preferred_username", + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + RoleAttributePath: "roles", + GroupsAttributePath: "groups[]", + OrgAttributePath: "orgs[]", + OrgMapping: []string{"org1:Org4:Editor", "org2:Org5:Viewer"}, + }, + }, + }, + { + name: "Invalid Use case with org_mapping and invalid roles", + wantID: &authn.Identity{ + OrgID: 0, + OrgName: "", + OrgRoles: map[int64]identity.RoleType{4: identity.RoleEditor, 5: identity.RoleViewer}, + Login: "eai-doe", + Groups: []string{"foo", "bar"}, + Name: "Eai Doe", + Email: "eai.doe@cor.po", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.JWTModule, + AuthID: "1234567890", + IsDisabled: false, + HelpFlags1: 0, + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + FetchSyncedUser: true, + SyncOrgRoles: true, + SyncPermissions: true, + SyncTeams: true, + LookUpParams: login.UserLookupParams{ + Email: stringPtr("eai.doe@cor.po"), + Login: stringPtr("eai-doe"), + }, + }, + }, + verifyProvider: func(context.Context, string) (map[string]any, error) { + return map[string]any{ + "sub": "1234567890", + "email": "eai.doe@cor.po", + "preferred_username": "eai-doe", + "name": "Eai Doe", + "roles": []string{"Invalid"}, + "groups": []string{"foo", "bar"}, + "orgs": []string{"org1", "org2"}, + }, nil + }, + cfg: &setting.Cfg{ + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + EmailClaim: "email", + UsernameClaim: "preferred_username", + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + RoleAttributePath: "roles", + GroupsAttributePath: "groups[]", + OrgAttributePath: "orgs[]", + OrgMapping: []string{"org1:Org4:Editor", "org2:Org5:Viewer"}, + }, + }, + }, } for _, tc := range testCases { @@ -146,7 +259,10 @@ func TestAuthenticateJWT(t *testing.T) { VerifyProvider: tc.verifyProvider, } - jwtClient := ProvideJWT(jwtService, tc.cfg) + jwtClient := ProvideJWT(jwtService, + connectors.ProvideOrgRoleMapper(tc.cfg, + &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), + tc.cfg) validHTTPReq := &http.Request{ Header: map[string][]string{ jwtHeaderName: {"sample-token"}}, @@ -262,7 +378,9 @@ func TestJWTClaimConfig(t *testing.T) { Header: map[string][]string{ jwtHeaderName: {token}}, } - jwtClient := ProvideJWT(jwtService, cfg) + jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, + &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), + cfg) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -372,7 +490,10 @@ func TestJWTTest(t *testing.T) { RoleAttributeStrict: true, }, } - jwtClient := ProvideJWT(jwtService, cfg) + jwtClient := ProvideJWT(jwtService, + connectors.ProvideOrgRoleMapper(cfg, + &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), + cfg) httpReq := &http.Request{ URL: &url.URL{RawQuery: "auth_token=" + tc.token}, Header: map[string][]string{ @@ -425,7 +546,10 @@ func TestJWTStripParam(t *testing.T) { httpReq := &http.Request{ URL: &url.URL{RawQuery: "auth_token=" + token + "&other_param=other_value"}, } - jwtClient := ProvideJWT(jwtService, cfg) + jwtClient := ProvideJWT(jwtService, + connectors.ProvideOrgRoleMapper(cfg, + &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), + cfg) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -481,7 +605,10 @@ func TestJWTSubClaimsConfig(t *testing.T) { }, } - jwtClient := ProvideJWT(jwtService, cfg) + jwtClient := ProvideJWT(jwtService, + connectors.ProvideOrgRoleMapper(cfg, + &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), + cfg) identity, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, diff --git a/pkg/services/authn/clients/provisioning.go b/pkg/services/authn/clients/provisioning.go new file mode 100644 index 00000000000..d50a0ff600c --- /dev/null +++ b/pkg/services/authn/clients/provisioning.go @@ -0,0 +1,62 @@ +package clients + +import ( + "context" + "fmt" + "regexp" + "time" + + claims "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/authn" +) + +var ( + _ authn.ContextAwareClient = (*Provisioning)(nil) + _ fmt.Stringer = (*Provisioning)(nil) // for debugging +) + +type Provisioning struct { + webhookRegexp *regexp.Regexp +} + +func ProvideProvisioning() *Provisioning { + // It's fine to compile a regexp here. The function is only called once per instance of APIBuilder, of which there should only ever be 1. + // Assumption: APIVERSION has no leading or trailing slashes. + webhookRegexp := regexp.MustCompile("^/apis/" + regexp.QuoteMeta(provisioning.APIVERSION) + "/namespaces/[^/]+/repositories/[^/]+/(webhook|render/.*)$") + return &Provisioning{webhookRegexp} +} + +func (p *Provisioning) String() string { + return p.Name() +} + +func (*Provisioning) Name() string { + return authn.ClientProvisioning +} + +func (p *Provisioning) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + return &authn.Identity{ + Type: claims.TypeAnonymous, + Name: p.Name(), + UID: p.Name(), + Login: p.Name(), + AuthID: p.Name(), + OrgID: r.OrgID, + AuthenticatedBy: authn.ClientProvisioning, + LastSeenAt: time.Now(), + }, nil +} + +func (*Provisioning) IsEnabled() bool { + return true +} + +func (p *Provisioning) Test(ctx context.Context, r *authn.Request) bool { + path := r.HTTPRequest.URL.Path + return p.webhookRegexp.MatchString(path) +} + +func (*Provisioning) Priority() uint { + return 5 // let most other clients go first +} diff --git a/pkg/services/authn/clients/provisioning_test.go b/pkg/services/authn/clients/provisioning_test.go new file mode 100644 index 00000000000..065e24a7343 --- /dev/null +++ b/pkg/services/authn/clients/provisioning_test.go @@ -0,0 +1,74 @@ +package clients + +import ( + "context" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/authn" +) + +func TestProvisioning_Test(t *testing.T) { + t.Parallel() + ctx := context.Background() + + for _, tt := range []struct { + Name, URL string + Valid bool + }{ + {"Root path is invalid", "https://grafana.localhost/", false}, + {"Non-provisioning path", "https://grafana.localhost/hello/world", false}, + {"Provisioning path that isn't webhook", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/unittest", false}, + {"Webhook path", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook", true}, + {"Webhook path with subpath", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook/unittest", false}, // this'll have to change if we ever want subpaths + {"Render image url", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/render/image-uid", true}, + } { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + svc := ProvideProvisioning() + + url, err := url.Parse(tt.URL) + require.NoError(t, err, "couldn't parse input URL") + req := &authn.Request{HTTPRequest: &http.Request{ + URL: url, + }} + + assert.Equal(t, tt.Valid, svc.Test(ctx, req)) + }) + } +} + +func TestProvisioning_Authenticate(t *testing.T) { + t.Parallel() + ctx := context.Background() + + url, err := url.Parse("https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook") + require.NoError(t, err, "couldn't parse URL known to be good?") + req := &authn.Request{HTTPRequest: &http.Request{ + URL: url, + }} + + svc := ProvideProvisioning() + identity, err := svc.Authenticate(ctx, req) + require.NoError(t, err, "Authenticate shouldn't err") + + assert.Equal(t, types.TypeAnonymous, identity.GetIdentityType(), "IdentityType") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.UID, "UID") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.Login, "Login") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.AuthenticatedBy, "AuthenticatedBy") + assert.Equal(t, false, identity.GetIsGrafanaAdmin(), "IsAdmin") +} + +func TestProvisioning_Assumptions(t *testing.T) { + t.Run("APIVERSION constant has no leading or trailing slashes", func(t *testing.T) { + assert.False(t, strings.HasPrefix(provisioning.APIVERSION, "/"), "found leading / in APIVERSION: %s", provisioning.APIVERSION) + assert.False(t, strings.HasSuffix(provisioning.APIVERSION, "/"), "found trailing / in APIVERSION: %s", provisioning.APIVERSION) + }) +} diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index fd971e7070f..2b6e7bfe533 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -76,6 +76,8 @@ type Identity struct { Permissions map[int64]map[string][]string // IDToken is a signed token representing the identity that can be forwarded to plugins and external services. IDToken string + // ExternalUID is the unique identifier for the entity in the external system. + ExternalUID string IDTokenClaims *authn.Claims[authn.IDTokenClaims] AccessTokenClaims *authn.Claims[authn.AccessTokenClaims] diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index 7635a193f7f..c930bb92fcb 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -63,6 +63,10 @@ func newMapper() mapper { "iam.grafana.app": { "teams": newResourceTranslation("teams", "id", false), }, + "secret.grafana.app": { + "securevalues": newResourceTranslation("secret.securevalues", "uid", false), + "keepers": newResourceTranslation("secret.keepers", "uid", false), + }, } } diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 910796e29f0..0c84c2d963e 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -103,6 +103,14 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv } ctx = request.WithNamespace(ctx, req.GetNamespace()) + span.SetAttributes( + attribute.String("subject", req.Subject), + attribute.String("namespace", checkReq.Namespace.Value), + attribute.String("action", checkReq.Action), + attribute.String("name", checkReq.Name), + attribute.String("folder", checkReq.ParentFolder), + ) + permissions, err := s.getIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) @@ -134,6 +142,12 @@ func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1. } ctx = request.WithNamespace(ctx, req.GetNamespace()) + span.SetAttributes( + attribute.String("subject", req.Subject), + attribute.String("namespace", listReq.Namespace.Value), + attribute.String("action", listReq.Action), + ) + permissions, err := s.getIdentityPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) diff --git a/pkg/services/authz/rbac/store/permission_query.sql b/pkg/services/authz/rbac/store/permission_query.sql index dfa82390541..fea4353c259 100644 --- a/pkg/services/authz/rbac/store/permission_query.sql +++ b/pkg/services/authz/rbac/store/permission_query.sql @@ -1,21 +1,21 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM {{ .Ident .PermissionTable }} as p +INNER JOIN ( + SELECT role_id FROM {{ .Ident .BuiltinRoleTable }} as br WHERE (br.role = {{ .Arg .Query.Role }} AND (br.org_id = {{ .Arg .Query.OrgID }} OR br.org_id = 0)) + {{ if .Query.IsServerAdmin }} + OR (br.role = 'Grafana Admin') + {{ end }} + {{ if .Query.UserID }} + UNION ALL + SELECT role_id FROM {{ .Ident .UserRoleTable }} as ur WHERE ur.user_id = {{ .Arg .Query.UserID }} AND (ur.org_id = {{ .Arg .Query.OrgID }} OR ur.org_id = 0) + {{ end }} + {{ if .Query.TeamIDs }} + UNION ALL + SELECT role_id FROM {{ .Ident .TeamRoleTable }} as tr WHERE tr.team_id IN ({{ .ArgList .Query.TeamIDs }}) AND tr.org_id = {{ .Arg .Query.OrgID }} + {{ end }} +) as roles ON p.role_id = roles.role_id WHERE {{ if .Query.ActionSets }} p.action IN ({{ .ArgList .Query.ActionSets }}, {{ .Arg .Query.Action }}) {{ else }} p.action = {{ .Arg .Query.Action }} - {{ end }} -AND p.role_id IN ( - SELECT role_id FROM {{ .Ident .BuiltinRoleTable }} as br WHERE (br.role = {{ .Arg .Query.Role }} AND (br.org_id = {{ .Arg .Query.OrgID }} OR br.org_id = 0)) - {{ if .Query.IsServerAdmin }} - OR (br.role = 'Grafana Admin') - {{ end }} - {{ if .Query.UserID }} - UNION - SELECT role_id FROM {{ .Ident .UserRoleTable }} as ur WHERE ur.user_id = {{ .Arg .Query.UserID }} AND (ur.org_id = {{ .Arg .Query.OrgID }} OR ur.org_id = 0) - {{ end }} - {{ if .Query.TeamIDs }} - UNION - SELECT role_id FROM {{ .Ident .TeamRoleTable }} as tr WHERE tr.team_id IN ({{ .ArgList .Query.TeamIDs }}) AND tr.org_id = {{ .Arg .Query.OrgID }} - {{ end }} -) + {{ end }} \ No newline at end of file diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-With_action_sets.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-With_action_sets.sql index 0393be57d3f..443e4b6f518 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-With_action_sets.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-With_action_sets.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +INNER JOIN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action IN ('folders:edit', 'folders:admin', 'folders:create') -AND p.role_id IN ( - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql index ba9f7064c91..39cdf20bd5d 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql @@ -1,9 +1,9 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +INNER JOIN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) + OR (br.role = 'Grafana Admin') + UNION ALL + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') - UNION - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql index 99959aeef2d..040afd50301 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql @@ -1,6 +1,6 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +INNER JOIN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) -) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql index d47a6b8226d..efd22a9b118 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql @@ -1,10 +1,10 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +INNER JOIN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) + UNION ALL + SELECT role_id FROM `grafana`.`team_role` as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION - SELECT role_id FROM `grafana`.`team_role` as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 -) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql index 15f8dde3169..17746dd3319 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +INNER JOIN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-With_action_sets.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-With_action_sets.sql index 8c35badfb31..5759d3b0017 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-With_action_sets.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-With_action_sets.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action IN ('folders:edit', 'folders:admin', 'folders:create') -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql index c78b0cbd2aa..45e8dc2b6fd 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql @@ -1,9 +1,9 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) + OR (br.role = 'Grafana Admin') + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql index 20544064211..8af7430873d 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql @@ -1,6 +1,6 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) -) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql index ae269975c83..e07071250ea 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql @@ -1,10 +1,10 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) + UNION ALL + SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION - SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 -) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql index 3a78a730602..a2a6695ef1d 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-With_action_sets.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-With_action_sets.sql index 8c35badfb31..5759d3b0017 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-With_action_sets.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-With_action_sets.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action IN ('folders:edit', 'folders:admin', 'folders:create') -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql index c78b0cbd2aa..45e8dc2b6fd 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql @@ -1,9 +1,9 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) + OR (br.role = 'Grafana Admin') + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql index 20544064211..8af7430873d 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql @@ -1,6 +1,6 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) -) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql index ae269975c83..e07071250ea 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql @@ -1,10 +1,10 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) + UNION ALL + SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION - SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 -) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql index 3a78a730602..a2a6695ef1d 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql @@ -1,8 +1,8 @@ SELECT p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +INNER JOIN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION ALL + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) +) as roles ON p.role_id = roles.role_id WHERE p.action = 'folders:read' -AND p.role_id IN ( - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) - UNION - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) -) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 246e74c6da7..b8f30c1cab4 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -628,62 +628,92 @@ func TestGetFolderNamesForFolderUIDs(t *testing.T) { func TestGetParentNames(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) + s := setUpServiceTest(t, false).(*Service) + user := &user.SignedInUser{OrgID: 1} - libraryElementFolderUID := "folderUID-A" + testcases := []struct { + name string fakeFolders []*folder.Folder - folders []folder.CreateFolderCommand - dashboards []dashboards.Dashboard - libraryElements []libraryElement - alertRules []alertRule - expectedParentNames map[cloudmigration.MigrateDataType][]string + folderHierarchy map[cloudmigration.MigrateDataType]map[string]string + expectedParentNames map[cloudmigration.MigrateDataType]map[string]string }{ { + name: "multiple data types", fakeFolders: []*folder.Folder{ - {UID: "folderUID-A", Title: "Folder A", OrgID: 1, ParentUID: ""}, - {UID: "folderUID-B", Title: "Folder B", OrgID: 1, ParentUID: "folderUID-A"}, - {UID: "folderUID-X", Title: "Folder X", OrgID: 1, ParentUID: ""}, + {UID: "folderUID-A", Title: "Folder A", OrgID: 1}, + {UID: "folderUID-B", Title: "Folder B", OrgID: 1}, + {UID: "folderUID-C", Title: "Folder C", OrgID: 1}, }, - folders: []folder.CreateFolderCommand{ - {UID: "folderUID-C", Title: "Folder A", OrgID: 1, ParentUID: "folderUID-A"}, + folderHierarchy: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "folderUID-A", "dashboard-2": "folderUID-B", "dashboard-3": ""}, + cloudmigration.LibraryElementDataType: {"libElement-1": "folderUID-A", "libElement-2": "folderUID-C"}, + cloudmigration.AlertRuleType: {"alertRule-1": "folderUID-B"}, }, - dashboards: []dashboards.Dashboard{ - {UID: "dashboardUID-0", OrgID: 1, FolderUID: ""}, - {UID: "dashboardUID-1", OrgID: 1, FolderUID: "folderUID-A"}, - {UID: "dashboardUID-2", OrgID: 1, FolderUID: "folderUID-B"}, + expectedParentNames: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "Folder A", "dashboard-2": "Folder B", "dashboard-3": ""}, + cloudmigration.LibraryElementDataType: {"libElement-1": "Folder A", "libElement-2": "Folder C"}, + cloudmigration.AlertRuleType: {"alertRule-1": "Folder B"}, }, - libraryElements: []libraryElement{ - {UID: "libraryElementUID-0", FolderUID: &libraryElementFolderUID}, - {UID: "libraryElementUID-1"}, + }, + { + name: "empty folder hierarchy", + fakeFolders: []*folder.Folder{ + {UID: "folderUID-A", Title: "Folder A", OrgID: 1}, }, - alertRules: []alertRule{ - {UID: "alertRuleUID-0", FolderUID: ""}, - {UID: "alertRuleUID-1", FolderUID: "folderUID-B"}, + folderHierarchy: map[cloudmigration.MigrateDataType]map[string]string{}, + expectedParentNames: map[cloudmigration.MigrateDataType]map[string]string{}, + }, + { + name: "all root folders (no parents)", + fakeFolders: []*folder.Folder{ + {UID: "folderUID-A", Title: "Folder A", OrgID: 1}, }, - expectedParentNames: map[cloudmigration.MigrateDataType][]string{ - cloudmigration.DashboardDataType: {"", "Folder A", "Folder B"}, - cloudmigration.FolderDataType: {"Folder A"}, - cloudmigration.LibraryElementDataType: {"Folder A"}, - cloudmigration.AlertRuleType: {"Folder B"}, + folderHierarchy: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "", "dashboard-2": ""}, + cloudmigration.LibraryElementDataType: {"libElement-1": ""}, + }, + expectedParentNames: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "", "dashboard-2": ""}, + cloudmigration.LibraryElementDataType: {"libElement-1": ""}, + }, + }, + { + name: "non-existent folder UIDs", + fakeFolders: []*folder.Folder{ + {UID: "folderUID-A", Title: "Folder A", OrgID: 1}, + }, + folderHierarchy: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "folderUID-A", "dashboard-2": "non-existent-uid"}, + }, + expectedParentNames: map[cloudmigration.MigrateDataType]map[string]string{ + cloudmigration.DashboardDataType: {"dashboard-1": "Folder A", "dashboard-2": ""}, }, }, } for _, tc := range testcases { - s.folderService = &foldertest.FakeService{ExpectedFolders: tc.fakeFolders} + t.Run(tc.name, func(t *testing.T) { + s.folderService = &foldertest.FakeService{ExpectedFolders: tc.fakeFolders} - dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.dashboards, tc.folders, tc.libraryElements, tc.alertRules) - require.NoError(t, err) + dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.folderHierarchy) + require.NoError(t, err) - for dataType, expectedParentNames := range tc.expectedParentNames { - actualParentNames := slices.Collect(maps.Values(dataUIDsToParentNamesByType[dataType])) - require.Len(t, actualParentNames, len(expectedParentNames)) - require.ElementsMatch(t, expectedParentNames, actualParentNames) - } + for dataType, expectedParentNames := range tc.expectedParentNames { + actualParentNames := dataUIDsToParentNamesByType[dataType] + + require.Equal(t, len(expectedParentNames), len(actualParentNames)) + + for uid, expectedName := range expectedParentNames { + actualName, exists := actualParentNames[uid] + require.True(t, exists) + require.Equal(t, expectedName, actualName) + } + } + }) } } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 034c601f510..e74c9217fbc 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -12,6 +12,9 @@ import ( "sort" "time" + "go.opentelemetry.io/otel/codes" + "golang.org/x/crypto/nacl/box" + snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src" "github.com/grafana/grafana-cloud-migration-snapshot/src/contracts" "github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto" @@ -29,9 +32,6 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/retryer" - "golang.org/x/crypto/nacl/box" - - "go.opentelemetry.io/otel/codes" ) var currentMigrationTypes = []cloudmigration.MigrateDataType{ @@ -52,6 +52,10 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getMigrationDataJSON") defer span.End() + migrationDataSlice := make([]cloudmigration.MigrateDataRequestItem, 0) + + folderHierarchy := make(map[cloudmigration.MigrateDataType]map[string]string, 0) + // Plugins plugins, err := s.getPlugins(ctx, signedInUser) if err != nil { @@ -59,74 +63,6 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } - // Data sources - dataSources, err := s.getDataSourceCommands(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get datasources", "err", err) - return nil, err - } - - // Dashboards and folders are linked via the schema, so we need to get both - dashs, folders, err := s.getDashboardAndFolderCommands(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get dashboards and folders", "err", err) - return nil, err - } - - libraryElements, err := s.getLibraryElementsCommands(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get library elements", "err", err) - return nil, err - } - - // Alerts: Mute Timings - muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert mute timings", "err", err) - return nil, err - } - - // Alerts: Notification Templates - notificationTemplates, err := s.getNotificationTemplates(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert notification templates", "err", err) - return nil, err - } - - // Alerts: Contact Points - contactPoints, err := s.getContactPoints(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert contact points", "err", err) - return nil, err - } - - // Alerts: Notification Policies - notificationPolicies, err := s.getNotificationPolicies(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert notification policies", "err", err) - return nil, err - } - - // Alerts: Alert Rule Groups - alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert rule groups", "err", err) - return nil, err - } - - // Alerts: Alert Rules - alertRules, err := s.getAlertRules(ctx, signedInUser) - if err != nil { - s.log.Error("Failed to get alert rules", "err", err) - return nil, err - } - - migrationDataSlice := make( - []cloudmigration.MigrateDataRequestItem, 0, - len(plugins)+len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+ - len(muteTimings)+len(notificationTemplates)+len(contactPoints)+len(alertRules), - ) - for _, plugin := range plugins { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.PluginDataType, @@ -136,6 +72,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Data sources + dataSources, err := s.getDataSourceCommands(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get datasources", "err", err) + return nil, err + } + for _, ds := range dataSources { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.DatasourceDataType, @@ -145,6 +88,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Dashboards & Folders: linked via the schema, so we need to get both + dashs, folders, err := s.getDashboardAndFolderCommands(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get dashboards and folders", "err", err) + return nil, err + } + + folderHierarchy[cloudmigration.DashboardDataType] = make(map[string]string, 0) + for _, dashboard := range dashs { dashboard.Data.Del("id") migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ @@ -159,8 +111,12 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S FolderUID: dashboard.FolderUID, }, }) + + folderHierarchy[cloudmigration.DashboardDataType][dashboard.UID] = dashboard.FolderUID } + folderHierarchy[cloudmigration.FolderDataType] = make(map[string]string, 0) + folders = sortFolders(folders) for _, f := range folders { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ @@ -169,8 +125,19 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S Name: f.Title, Data: f, }) + + folderHierarchy[cloudmigration.FolderDataType][f.UID] = f.ParentUID } + // Library Elements + libraryElements, err := s.getLibraryElementsCommands(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get library elements", "err", err) + return nil, err + } + + folderHierarchy[cloudmigration.LibraryElementDataType] = make(map[string]string, 0) + for _, libraryElement := range libraryElements { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.LibraryElementDataType, @@ -178,6 +145,17 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S Name: libraryElement.Name, Data: libraryElement, }) + + if libraryElement.FolderUID != nil { + folderHierarchy[cloudmigration.LibraryElementDataType][libraryElement.UID] = *libraryElement.FolderUID + } + } + + // Alerts: Mute Timings + muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert mute timings", "err", err) + return nil, err } for _, muteTiming := range muteTimings { @@ -189,6 +167,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Alerts: Notification Templates + notificationTemplates, err := s.getNotificationTemplates(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert notification templates", "err", err) + return nil, err + } + for _, notificationTemplate := range notificationTemplates { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.NotificationTemplateType, @@ -198,6 +183,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Alerts: Contact Points + contactPoints, err := s.getContactPoints(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert contact points", "err", err) + return nil, err + } + for _, contactPoint := range contactPoints { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.ContactPointType, @@ -207,6 +199,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Alerts: Notification Policies + notificationPolicies, err := s.getNotificationPolicies(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert notification policies", "err", err) + return nil, err + } + if len(notificationPolicies.Name) > 0 { // Notification Policy can only be managed by updating its entire tree, so we send the whole thing as one item. migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ @@ -217,6 +216,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Alerts: Alert Rule Groups + alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert rule groups", "err", err) + return nil, err + } + for _, alertRuleGroup := range alertRuleGroups { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.AlertRuleGroupType, @@ -226,6 +232,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Alerts: Alert Rules + alertRules, err := s.getAlertRules(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert rules", "err", err) + return nil, err + } + + folderHierarchy[cloudmigration.AlertRuleType] = make(map[string]string, 0) + for _, alertRule := range alertRules { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.AlertRuleType, @@ -233,10 +248,12 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S Name: alertRule.Title, Data: alertRule, }) + + folderHierarchy[cloudmigration.AlertRuleType][alertRule.UID] = alertRule.FolderUID } - // Obtain the names of parent elements for Dashboard and Folders data types - parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements, alertRules) + // Obtain the names of parent elements for data types that have folders. + parentNamesByType, err := s.getParentNames(ctx, signedInUser, folderHierarchy) if err != nil { s.log.Error("Failed to get parent folder names", "err", err) } @@ -805,10 +822,7 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser func (s *Service) getParentNames( ctx context.Context, signedInUser *user.SignedInUser, - dashboards []dashboards.Dashboard, - folders []folder.CreateFolderCommand, - libraryElements []libraryElement, - alertRules []alertRule, + folderHierarchy map[cloudmigration.MigrateDataType]map[string]string, ) (map[cloudmigration.MigrateDataType]map[string](string), error) { parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string]string) for _, dataType := range currentMigrationTypes { @@ -817,25 +831,18 @@ func (s *Service) getParentNames( // Obtain list of unique folderUIDs parentFolderUIDsSet := make(map[string]struct{}) - for _, dashboard := range dashboards { - // we dont need the root folder - if dashboard.FolderUID != "" { - parentFolderUIDsSet[dashboard.FolderUID] = struct{}{} - } - } - for _, f := range folders { - parentFolderUIDsSet[f.ParentUID] = struct{}{} - } - for _, libraryElement := range libraryElements { - if libraryElement.FolderUID != nil { - parentFolderUIDsSet[*libraryElement.FolderUID] = struct{}{} - } - } - for _, alertRule := range alertRules { - if alertRule.FolderUID != "" { - parentFolderUIDsSet[alertRule.FolderUID] = struct{}{} + + for _, folderUIDs := range folderHierarchy { + for _, folderUID := range folderUIDs { + // Skip the root folder + if folderUID == "" { + continue + } + + parentFolderUIDsSet[folderUID] = struct{}{} } } + parentFolderUIDsSlice := make([]string, 0, len(parentFolderUIDsSet)) for parentFolderUID := range parentFolderUIDsSet { parentFolderUIDsSlice = append(parentFolderUIDsSlice, parentFolderUID) @@ -849,20 +856,9 @@ func (s *Service) getParentNames( } // Prepare map of {data type: {data UID : parentName}} - for _, dashboard := range dashboards { - parentNamesByType[cloudmigration.DashboardDataType][dashboard.UID] = foldersUIDsToFolderName[dashboard.FolderUID] - } - for _, f := range folders { - parentNamesByType[cloudmigration.FolderDataType][f.UID] = foldersUIDsToFolderName[f.ParentUID] - } - for _, libraryElement := range libraryElements { - if libraryElement.FolderUID != nil { - parentNamesByType[cloudmigration.LibraryElementDataType][libraryElement.UID] = foldersUIDsToFolderName[*libraryElement.FolderUID] - } - } - for _, alertRule := range alertRules { - if alertRule.FolderUID != "" { - parentNamesByType[cloudmigration.AlertRuleType][alertRule.UID] = foldersUIDsToFolderName[alertRule.FolderUID] + for dataType, uidFolderMap := range folderHierarchy { + for uid, folderUID := range uidFolderMap { + parentNamesByType[dataType][uid] = foldersUIDsToFolderName[folderUID] } } diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 92232c95bd8..b904e3c2e07 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -35,6 +35,7 @@ type DashboardService interface { GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUid string) error RestoreDashboard(ctx context.Context, dashboard *Dashboard, user identity.Requester, optionalFolderUID string) error + CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error CleanUpDeletedDashboards(ctx context.Context) (int64, error) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*Dashboard, error) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 2e37ea6f3b8..ad526998488 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -562,6 +562,24 @@ func (_m *FakeDashboardService) SoftDeleteDashboard(ctx context.Context, orgID i return r0 } +// CleanUpDashboard provides a mock function with given fields: ctx, dashboardUID, orgId +func (_m *FakeDashboardService) CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error { + ret := _m.Called(ctx, dashboardUID, orgId) + + if len(ret) == 0 { + panic("no return value specified for CleanUpDashboard") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok { + r0 = rf(ctx, dashboardUID, orgId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewFakeDashboardService creates a new instance of FakeDashboardService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFakeDashboardService(t interface { diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index d5972f16bc3..032d574b07f 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "time" @@ -673,7 +674,7 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, {SQL: "DELETE FROM dashboard_tag WHERE dashboard_uid = ? AND org_id = ?", args: []any{dashboard.UID, dashboard.OrgID}}, {SQL: "DELETE FROM star WHERE dashboard_id = ? ", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard WHERE id = ?", args: []any{dashboard.ID}}, - {SQL: "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", args: []any{dashboard.ID}}, + {SQL: "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", args: []any{strconv.FormatInt(dashboard.ID, 10)}}, // Column has TEXT type. {SQL: "DELETE FROM dashboard_version WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_acl WHERE dashboard_id = ?", args: []any{dashboard.ID}}, @@ -737,7 +738,7 @@ func (d *dashboardStore) CleanupAfterDelete(ctx context.Context, cmd *dashboards sqlStatements := []statement{ {SQL: "DELETE FROM dashboard_tag WHERE dashboard_uid = ? AND org_id = ?", args: []any{cmd.UID, cmd.OrgID}}, {SQL: "DELETE FROM star WHERE dashboard_uid = ? AND org_id = ?", args: []any{cmd.UID, cmd.OrgID}}, - {SQL: "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", args: []any{cmd.ID}}, + {SQL: "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", args: []any{strconv.FormatInt(cmd.ID, 10)}}, // Column has TEXT type. {SQL: "DELETE FROM dashboard_version WHERE dashboard_id = ?", args: []any{cmd.ID}}, {SQL: "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", args: []any{cmd.ID}}, {SQL: "DELETE FROM dashboard_acl WHERE dashboard_id = ?", args: []any{cmd.ID}}, diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 0128d7d2e17..fd074aba343 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -265,8 +265,6 @@ type GetDashboardQuery struct { FolderID *int64 FolderUID *string OrgID int64 - - IncludeDeleted bool // only supported when using unified storage } type DashboardTagCloudItem struct { diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 16c68cff3e3..d85dd62405b 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -11,7 +11,6 @@ import ( "time" "github.com/google/uuid" - "github.com/grafana/grafana/pkg/util/retryer" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "golang.org/x/exp/maps" @@ -30,9 +29,13 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/client" @@ -42,7 +45,6 @@ import ( dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota" @@ -56,6 +58,8 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/retryer" + "go.opentelemetry.io/otel/attribute" ) var ( @@ -68,6 +72,11 @@ var ( tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service") ) +const ( + k8sDashboardKvNamespace = "dashboard-cleanup" + k8sDashboardKvLastResourceVersionKey = "last-resource-version" +) + type DashboardServiceImpl struct { cfg *setting.Cfg log log.Logger @@ -82,11 +91,271 @@ type DashboardServiceImpl struct { k8sclient client.K8sHandler metrics *dashboardsMetrics publicDashboardService publicdashboards.ServiceWrapper + serverLockService *serverlock.ServerLockService + kvstore kvstore.KVStore dashboardPermissionsReady chan struct{} } +func (dr *DashboardServiceImpl) startK8sDeletedDashboardsCleanupJob(ctx context.Context) chan struct{} { + done := make(chan struct{}) + go func() { + defer close(done) + + ticker := time.NewTicker(dr.cfg.K8sDashboardCleanup.Interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := dr.executeCleanupWithLock(ctx); err != nil { + dr.log.Error("Failed to execute k8s dashboard cleanup", "error", err) + } + } + } + }() + return done +} + +func (dr *DashboardServiceImpl) executeCleanupWithLock(ctx context.Context) error { + // We're taking a leader-like locking approach here. By locking and executing, but never releasing the lock, + // we ensure that other instances of this service can't run in parallel and hence the cleanup will only happen once + // per cleanup interval by setting the maxInterval and having the time between executions be the cleanup interval as well. + return dr.serverLockService.LockAndExecute( + ctx, + k8sDashboardKvNamespace, + dr.cfg.K8sDashboardCleanup.Interval, + func(ctx context.Context) { + if err := dr.cleanupK8sDashboardResources(ctx, dr.cfg.K8sDashboardCleanup.BatchSize, dr.cfg.K8sDashboardCleanup.Timeout); err != nil { + dr.log.Error("Failed to cleanup k8s dashboard resources", "error", err) + } + }, + ) +} + +// cleanupK8sDashboardResources cleans up resources marked for deletion in the k8s API. +// It processes all organizations, finds dashboards with the trash label, and cleans them up. +// batchSize specifies how many dashboards to process in a single batch. +// timeout specifies the timeout duration for the cleanup operation. +func (dr *DashboardServiceImpl) cleanupK8sDashboardResources(ctx context.Context, batchSize int64, timeout time.Duration) error { + ctx, span := tracer.Start(ctx, "dashboards.service.cleanupK8sDashboardResources") + defer span.End() + + if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { + return nil + } + + // Create a timeout context to ensure we complete before the lock expires + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return err + } + dr.log.Debug("Running k8s dashboard resource cleanup for all orgs", "numOrgs", len(orgs)) + + var errs []error + for _, org := range orgs { + // Check if we're approaching the timeout + if ctx.Err() != nil { + dr.log.Info("Timeout reached during cleanup, stopping processing", "timeout", timeout) + break + } + + orgErr := dr.cleanupOrganizationK8sDashboards(ctx, org.ID, batchSize) + if orgErr != nil { + errs = append(errs, fmt.Errorf("org %d: %w", org.ID, orgErr)) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +// cleanupOrganizationK8sDashboards handles cleanup for a single organization's Kubernetes dashboards +func (dr *DashboardServiceImpl) cleanupOrganizationK8sDashboards(ctx context.Context, orgID int64, batchSize int64) error { + dr.log.Debug("Running k8s dashboard resource cleanup for org", "orgID", orgID) + + ctx, span := tracer.Start(ctx, "dashboards.service.cleanupK8sDashboardResources.org") + defer span.End() + span.SetAttributes(attribute.Int64("org_id", orgID)) + + ctx, _ = identity.WithServiceIdentity(ctx, orgID) + + // Get the last processed resource version + lastResourceVersion, err := dr.getLastResourceVersion(ctx, orgID) + if err != nil { + return err + } + + var errs []error + continueToken := "" + itemsProcessed := 0 + + for { + // Check if we're approaching the timeout + if ctx.Err() != nil { + dr.log.Info("Timeout reached during org cleanup, stopping processing", "orgID", orgID) + break + } + + // List resources to be cleaned up + data, listErr, shouldContinue := dr.listResourcesToCleanup(ctx, orgID, lastResourceVersion, continueToken, batchSize) + if listErr != nil { + errs = append(errs, fmt.Errorf("failed to list resources: %w", listErr)) + break + } + if shouldContinue { + // Reset and try again with updated resource version + lastResourceVersion = "0" + continueToken = "" + continue + } + + // Skip the first item if it matches our last resource version (due to NotOlderThan behavior) + if len(data.Items) > 0 && data.Items[0].GetResourceVersion() == lastResourceVersion { + data.Items = data.Items[1:] + } + + if len(data.Items) == 0 { + dr.log.Debug("No items to clean up in this batch", "orgID", orgID) + break + } + + dr.log.Info("Processing dashboard cleanup batch", "orgID", orgID, "count", len(data.Items)) + + // Process the batch + processedItems, processingErrs := dr.processDashboardBatch(ctx, orgID, data.Items) + if len(processingErrs) > 0 { + errs = append(errs, processingErrs...) + } + itemsProcessed += processedItems + + // Update resource version after the batch + if len(data.Items) > 0 { + maxBatchResourceVersion := data.Items[len(data.Items)-1].GetResourceVersion() + if lastResourceVersion != maxBatchResourceVersion { + dr.log.Info("Updating resource version after batch", "orgID", orgID, + "newResourceVersion", maxBatchResourceVersion, "oldResourceVersion", lastResourceVersion) + + if updateErr := dr.kvstore.Set(ctx, orgID, k8sDashboardKvNamespace, + k8sDashboardKvLastResourceVersionKey, maxBatchResourceVersion); updateErr != nil { + errs = append(errs, fmt.Errorf("failed to update resource version: %w", updateErr)) + } + } + } + + meta, _ := data.Object["metadata"].(map[string]interface{}) + continueToken, _ = meta["continue"].(string) + if continueToken == "" { + break + } + } + + if itemsProcessed > 0 { + dr.log.Info("Finished k8s dashboard resources cleanup", "orgID", orgID, "itemsProcessed", itemsProcessed) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +// getLastResourceVersion retrieves the last processed resource version from kvstore +func (dr *DashboardServiceImpl) getLastResourceVersion(ctx context.Context, orgID int64) (string, error) { + lastResourceVersion, ok, err := dr.kvstore.Get(ctx, orgID, k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey) + if err != nil { + return "", fmt.Errorf("failed to get last resource version: %w", err) + } + + if !ok { + dr.log.Info("No last resource version found, starting from scratch", "orgID", orgID) + return "0", nil + } + + return lastResourceVersion, nil +} + +// listResourcesToCleanup lists resources that need to be cleaned up +func (dr *DashboardServiceImpl) listResourcesToCleanup(ctx context.Context, orgID int64, resourceVersion, continueToken string, batchSize int64) (*unstructured.UnstructuredList, error, bool) { + var listOptions v1.ListOptions + if continueToken != "" { + listOptions = v1.ListOptions{ + LabelSelector: utils.LabelKeyGetTrash + "=true", + Continue: continueToken, + Limit: batchSize, + } + } else { + listOptions = v1.ListOptions{ + LabelSelector: utils.LabelKeyGetTrash + "=true", + ResourceVersionMatch: v1.ResourceVersionMatchNotOlderThan, + ResourceVersion: resourceVersion, + Limit: batchSize, + } + } + + data, err := dr.k8sclient.List(ctx, orgID, listOptions) + if err != nil { + if strings.Contains(err.Error(), "too old resource version") { + // If the resource version is too old, start from the current version + dr.log.Info("Resource version too old, starting from current version", "orgID", orgID) + return nil, nil, true // Signal to continue with reset version + } + return nil, err, false + } + + return data, nil, false +} + +// processDashboardBatch processes a batch of dashboards for cleanup +func (dr *DashboardServiceImpl) processDashboardBatch(ctx context.Context, orgID int64, items []unstructured.Unstructured) (int, []error) { + var errs []error + itemsProcessed := 0 + + for _, item := range items { + dash, err := dr.UnstructuredToLegacyDashboard(ctx, &item, orgID) + if err != nil { + errs = append(errs, fmt.Errorf("failed to convert dashboard: %w", err)) + continue + } + + meta, _ := item.Object["metadata"].(map[string]interface{}) + deletionTimestamp, _ := meta["deletionTimestamp"].(string) + resourceVersion, _ := meta["resourceVersion"].(string) + + dr.log.Info("K8s dashboard resource previously got deleted, cleaning up", + "UID", dash.UID, + "orgID", orgID, + "deletionTimestamp", deletionTimestamp, + "resourceVersion", resourceVersion) + + if err = dr.CleanUpDashboard(ctx, dash.UID, orgID); err != nil { + errs = append(errs, fmt.Errorf("failed to clean up dashboard %s: %w", dash.UID, err)) + } + itemsProcessed++ + } + + return itemsProcessed, errs +} + +// This gets auto-invoked when grafana starts, part of the BackgroundService interface +func (dr *DashboardServiceImpl) Run(ctx context.Context) error { + cleanupBackgroundJobStopped := dr.startK8sDeletedDashboardsCleanupJob(ctx) + <-ctx.Done() + // Wait for cleanup job to finish + <-cleanupBackgroundJobStopped + return ctx.Err() +} + var _ dashboards.PermissionsRegistrationService = (*DashboardServiceImpl)(nil) +var _ registry.BackgroundService = (*DashboardServiceImpl)(nil) // This is the uber service that implements a three smaller services func ProvideDashboardServiceImpl( @@ -96,6 +365,8 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, resourceClient resource.ResourceClient, dual dualwrite.Service, sorter sort.Service, + serverLockService *serverlock.ServerLockService, + kvstore kvstore.KVStore, ) (*DashboardServiceImpl, error) { k8sHandler := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient, sorter) @@ -113,6 +384,8 @@ func ProvideDashboardServiceImpl( metrics: newDashboardsMetrics(r), dashboardPermissionsReady: make(chan struct{}), publicDashboardService: publicDashboardService, + serverLockService: serverLockService, + kvstore: kvstore, } defaultLimits, err := readQuotaConfig(cfg) @@ -412,14 +685,24 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if isParentFolderChanged { - // Check that the user is allowed to add a dashboard to the folder - guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User) - if err != nil { - return nil, err + if canCreate, err := dr.canCreateDashboard(ctx, dto.User, dash); err != nil || !canCreate { + if err != nil { + return nil, err + } + return nil, dashboards.ErrDashboardUpdateAccessDenied } + } + + if dash.ID == 0 { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave { + if canCreate, err := dr.canCreateDashboard(ctx, dto.User, dash); err != nil || !canCreate { + if err != nil { + return nil, err + } + return nil, dashboards.ErrDashboardUpdateAccessDenied + } + } else { + if canSave, err := dr.canSaveDashboard(ctx, dto.User, dash); err != nil || !canSave { if err != nil { return nil, err } @@ -438,29 +721,6 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } } - guard, err := getGuardianForSavePermissionCheck(ctx, dash, dto.User) - if err != nil { - return nil, err - } - - if dash.ID == 0 { - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate { - if err != nil { - return nil, err - } - return nil, dashboards.ErrDashboardUpdateAccessDenied - } - } else { - if canSave, err := guard.CanSave(); err != nil || !canSave { - if err != nil { - return nil, err - } - return nil, dashboards.ErrDashboardUpdateAccessDenied - } - } - var userID int64 if id, err := identity.UserIdentifier(dto.User.GetID()); err == nil { userID = id @@ -560,11 +820,36 @@ func (dr *DashboardServiceImpl) ValidateDashboardBeforeSave(ctx context.Context, return isParentFolderChanged, nil } +func (dr *DashboardServiceImpl) canSaveDashboard(ctx context.Context, user identity.Requester, dash *dashboards.Dashboard) (bool, error) { + action := dashboards.ActionDashboardsWrite + if dash.IsFolder { + action = dashboards.ActionFoldersWrite + } + scope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID) + if dash.IsFolder { + scope = dashboards.ScopeFoldersProvider.GetResourceScopeUID(dash.UID) + } + return dr.ac.Evaluate(ctx, user, accesscontrol.EvalPermission(action, scope)) +} + +func (dr *DashboardServiceImpl) canCreateDashboard(ctx context.Context, user identity.Requester, dash *dashboards.Dashboard) (bool, error) { + action := dashboards.ActionDashboardsCreate + if dash.IsFolder { + action = dashboards.ActionFoldersCreate + } + scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(dash.FolderUID) + if dash.FolderUID == "" { + scope = dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID) + } + return dr.ac.Evaluate(ctx, user, accesscontrol.EvalPermission(action, scope)) +} + // waitForSearchQuery waits for the search query to return the expected number of hits. // Since US doesn't offer search-after-write guarantees, we can use this to wait after writes until the indexer is up to date. func (dr *DashboardServiceImpl) waitForSearchQuery(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery, maxRetries int, expectedHits int64) error { return retryer.Retry(func() (retryer.RetrySignal, error) { results, err := dr.searchDashboardsThroughK8sRaw(ctx, query) + dr.log.Debug("waitForSearchQuery", "dashboardUIDs", strings.Join(query.DashboardUIDs, ","), "total_hits", results.TotalHits, "err", err) if err != nil { return retryer.FuncError, err } @@ -594,6 +879,7 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. if err != nil { return err } + dr.log.Debug("Found dashboards to be deleted", "orgId", org.ID, "count", len(foundDashs)) // delete them var deletedUids []string @@ -603,10 +889,12 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. } deletedUids = append(deletedUids, foundDash.DashboardUID) } - // wait for deleted dashboards to be removed from the index - err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0) - if err != nil { - return err + if len(deletedUids) > 0 { + // wait for deleted dashboards to be removed from the index + err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0) + if err != nil { + return err + } } } return nil @@ -615,46 +903,6 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) } -// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard -// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() -func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) { - ctx, span := tracer.Start(ctx, "dashboards.service.getGuardianForSavePermissionCheck") - defer span.End() - - newDashboard := d.ID == 0 - - if newDashboard { - // if it's a new dashboard/folder check the parent folder permissions - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - guard, err := guardian.NewByFolder(ctx, &folder.Folder{ - ID: d.FolderID, // nolint:staticcheck - OrgID: d.OrgID, - }, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil - } - - if d.IsFolder { - guard, err := guardian.NewByFolder(ctx, &folder.Folder{ - ID: d.ID, // nolint:staticcheck - UID: d.UID, - OrgID: d.OrgID, - }, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil - } - - guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil -} - func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboards.Dashboard) error { if minRefreshInterval == "" { return nil @@ -782,7 +1030,7 @@ func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboar func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) { if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid, IncludeDeleted: true}) + return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid}) } return dr.dashboardStore.GetSoftDeletedDashboard(ctx, orgID, uid) @@ -885,18 +1133,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID} if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - err := dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard) - if err != nil { - return err - } - - // cleanup things related to dashboards that are not stored in unistore yet - err = dr.publicDashboardService.DeleteByDashboardUIDs(ctx, orgId, []string{dashboardUID}) - if err != nil { - return err - } - - return dr.dashboardStore.CleanupAfterDelete(ctx, cmd) + return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard) } if validateProvisionedDashboard { @@ -1295,6 +1532,14 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb Tags: hit.Tags, } + if hit.Field != nil && query.Sort.Name != "" { + fieldName, _, err := legacysearcher.ParseSortName(query.Sort.Name) + if err != nil { + return nil, err + } + result.SortMeta = hit.Field.GetNestedInt64(fieldName) + } + if hit.Resource == folderv0alpha1.RESOURCE { result.IsFolder = true } @@ -1511,6 +1756,19 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64 func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard } +func (dr *DashboardServiceImpl) CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error { + ctx, span := tracer.Start(ctx, "dashboards.service.CleanUpDashboard") + defer span.End() + + // cleanup things related to dashboards that are not stored in unistore yet + var err = dr.publicDashboardService.DeleteByDashboardUIDs(ctx, orgId, []string{dashboardUID}) + if err != nil { + return err + } + + return dr.dashboardStore.CleanupAfterDelete(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgId, UID: dashboardUID}) +} + func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (int64, error) { ctx, span := tracer.Start(ctx, "dashboards.service.CleanUpDeletedDashboards") defer span.End() @@ -1537,12 +1795,6 @@ func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (i // ----------------------------------------------------------------------------------------- func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { - // if including deleted dashboards for restore, use the /latest subresource - subresource := "" - if query.IncludeDeleted && dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) { - subresource = "latest" - } - // get uid if not passed in if query.UID == "" { result, err := dr.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{ @@ -1555,7 +1807,7 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer query.UID = result.UID } - out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, v1.GetOptions{}, subresource) + out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, v1.GetOptions{}, "") if err != nil && !apierrors.IsNotFound(err) { return nil, err } else if err != nil || out == nil { @@ -1593,12 +1845,13 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C meta.SetManagerProperties(m) meta.SetSourceProperties(s) - out, err := dr.createOrUpdateDash(ctx, obj, cmd.OrgID) + // Update will create if not exists (upsert!) + out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID) if err != nil { return nil, err } - return out, nil + return dr.UnstructuredToLegacyDashboard(ctx, out, cmd.OrgID) } func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd *dashboards.SaveDashboardCommand, orgID int64) (*dashboards.Dashboard, error) { @@ -1609,35 +1862,13 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd dashboard.SetPluginIDMeta(obj, cmd.PluginID) - out, err := dr.createOrUpdateDash(ctx, obj, orgID) + // Update will create if not exists (upsert!) + out, err := dr.k8sclient.Update(ctx, obj, orgID) if err != nil { return nil, err } - return out, nil -} - -func (dr *DashboardServiceImpl) createOrUpdateDash(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) { - var out *unstructured.Unstructured - current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID, v1.GetOptions{}) - if current == nil || err != nil { - out, err = dr.k8sclient.Create(ctx, obj, orgID) - if err != nil { - return nil, err - } - } else { - out, err = dr.k8sclient.Update(ctx, obj, orgID) - if err != nil { - return nil, err - } - } - - finalDash, err := dr.UnstructuredToLegacyDashboard(ctx, out, orgID) - if err != nil { - return nil, err - } - - return finalDash, nil + return dr.UnstructuredToLegacyDashboard(ctx, out, orgID) } func (dr *DashboardServiceImpl) deleteAllDashboardThroughK8s(ctx context.Context, orgID int64) error { @@ -1840,12 +2071,12 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Federated = []*resource.ResourceKey{federate} } - // technically, there exists the ability to register multiple ways of sorting using the legacy database - // see RegisterSortOption in pkg/services/search/sorting.go - // however, it doesn't look like we are taking advantage of that. And since by default the legacy - // sql will sort by title ascending, we only really need to handle the "alpha-desc" case - if query.Sort.Name == "alpha-desc" { - request.SortBy = append(request.SortBy, &resource.ResourceSearchRequest_Sort{Field: resource.SEARCH_FIELD_TITLE, Desc: true}) + if query.Sort.Name != "" { + sortName, isDesc, err := legacysearcher.ParseSortName(query.Sort.Name) + if err != nil { + return dashboardv0alpha1.SearchResults{}, err + } + request.SortBy = append(request.SortBy, &resource.ResourceSearchRequest_Sort{Field: sortName, Desc: isDesc}) } res, err := dr.k8sclient.Search(ctx, query.OrgId, request) diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 221ade24f72..7887e628110 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -8,13 +8,14 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" @@ -23,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -49,809 +49,710 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { // Basic validation tests - permissionScenario(t, "When saving a dashboard with non-existing id", true, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": float64(123412321), - "title": "Expect error", - }), - } + permissionScenario(t, "When saving a dashboard with non-existing id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": float64(123412321), + "title": "Expect error", + }), + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardNotFound, err) + }) // Given other organization t.Run("Given organization B", func(t *testing.T) { const otherOrgId int64 = 2 - permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", - true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Expect error", - }), - Overwrite: false, - } + permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "title": "Expect error", + }), + Overwrite: false, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) - - permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", - true, func(t *testing.T, sc *permissionScenarioContext) { - const otherOrgId int64 = 2 - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash with existing uid in other org", - }), - Overwrite: false, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - OrgID: otherOrgId, - UID: sc.savedDashInFolder.UID, - }) - require.NoError(t, err) - }) - }) - - t.Run("Given user has no permission to save", func(t *testing.T) { - const canSave = false - - permissionScenario(t, "When creating a new dashboard in the General folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - sqlStore := db.InitTestDB(t) - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, "", sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.otherSavedFolder.ID, sc.dashboardGuardianMock.DashID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() - - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "New dash", - }), - FolderUID: sc.savedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Dash", - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Dash", - }), - FolderUID: sc.savedDashInFolder.FolderUID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Dash", - }), - FolderUID: "", - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInGeneralFolder.UID, - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash", - }), - FolderUID: "", - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardNotFound, err) + }) + + permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", func(t *testing.T, sc *permissionScenarioContext) { + const otherOrgId int64 = 2 + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Dash with existing uid in other org", + }), + Overwrite: false, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + OrgID: otherOrgId, + UID: sc.savedDashInFolder.UID, }) + require.NoError(t, err) + }) }) t.Run("Given user has permission to save", func(t *testing.T) { - const canSave = true - t.Run("and overwrite flag is set to false", func(t *testing.T) { const shouldOverwrite = false - permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) - permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When creating a folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.True(t, res.IsFolder) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash without id and uid", + }), + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + assert.Greater(t, res.ID, int64(0)) + assert.NotEmpty(t, res.UID) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard when dashboard id is zero ", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": 0, + "title": "Dash with zero id", + }), + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard in non-existing folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Expect error", + }), + FolderUID: "123412321", + Overwrite: shouldOverwrite, + } + + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrFolderNotFound, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "test dash 23", + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } + + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id with current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "Updated title", + "version": sc.savedDashInGeneralFolder.Version, + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) - permissionScenario(t, "When creating a folder with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "test dash 23", + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.True(t, res.IsFolder) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) + }) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + permissionScenario(t, "When updating an existing dashboard by uid with current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Updated title", + "version": sc.savedDashInFolder.Version, + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash without id and uid", - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: sc.savedDashInFolder.FolderUID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - assert.Greater(t, res.ID, int64(0)) - assert.NotEmpty(t, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedDashInGeneralFolder.FolderUID, + Overwrite: shouldOverwrite, + } - permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": 0, - "title": "Dash with zero id", - }), - Overwrite: shouldOverwrite, - } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When creating a folder with same name as existing folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When saving a dashboard in non-existing folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Expect error", - }), - FolderUID: "123412321", - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrFolderNotFound, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "test dash 23", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id with current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - "version": sc.savedDashInGeneralFolder.Version, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "test dash 23", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - "version": sc.savedDashInFolder.Version, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a folder with same name as existing folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) }) t.Run("and overwrite flag is set to true", func(t *testing.T) { const shouldOverwrite = true - permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "Updated title", + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Updated title", + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating uid for existing dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": "new-uid", - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "uid": "new-uid", + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, "new-uid", res.UID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, "new-uid", res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": sc.savedDashInGeneralFolder.UID, - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "uid": sc.savedDashInGeneralFolder.UID, + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + t.Skip() + + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: sc.savedDashInFolder.FolderUID, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInFolder.UID, res.UID) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + t.Skip() - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedDashInGeneralFolder.FolderUID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInFolder.UID, res.UID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() + permissionScenario(t, "When updating existing folder to a dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedFolder.ID, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) + permissionScenario(t, "When updating existing dashboard to a folder using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing folder to a dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedFolder.ID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing folder to a dashboard using uid", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedFolder.UID, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing dashboard to a folder using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing dashboard to a folder using uid", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedFolder.UID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing folder to a dashboard using title", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": sc.savedFolder.Title, + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing dashboard to a folder using title", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": sc.savedDashInGeneralFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing folder to a dashboard using title", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedFolder.Title, - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating existing dashboard to a folder using title", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedDashInGeneralFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) }) }) }) } +func TestIntegrationDashboardServicePermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { + permissionScenario(t, "When creating a new dashboard in the General folder, requires create permissions scoped to the general folder", + func(t *testing.T, sc *permissionScenarioContext) { + sqlStore := db.InitTestDB(t) + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash", + }), + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsAll}, + }, + } + _, err := callSaveWithResult(t, cmd, sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When creating a new dashboard in other folder, requires create permissions scoped to the other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash", + }), + FolderUID: sc.otherSavedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("different_folder_uid")}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When creating a new dashboard by existing UID in folder, requires write permissions on the existing dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "New dash", + }), + FolderUID: sc.savedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID("different_dash_uid")}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, requires dashboard creation permissions on the destination folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInGeneralFolder.UID, + "title": "Dash", + }), + FolderUID: sc.otherSavedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + // Perms to write dashboard but not create dashboards in the destination folder + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to create dashboards in the destination folder but not write the dashboard + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to write dashboard and create dashboards in the destination folder + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When moving a dashboard by existing uid to the General folder from other folder, requires dashboard creation permissions on the general folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Dash", + }), + FolderUID: "", + UserID: 10000, + Overwrite: true, + } + + // Perms to write dashboard but not create dashboards in the destination folder + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to create dashboards in the destination folder but not write the dashboard + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to write dashboard and create dashboards in the destination folder + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.NoError(t, err) + }) + }) +} + type permissionScenarioContext struct { - dashboardGuardianMock *guardian.FakeDashboardGuardian sqlStore db.DB dashboardStore dashboards.Store savedFolder *dashboards.Dashboard @@ -862,14 +763,9 @@ type permissionScenarioContext struct { type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) -func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionScenarioFunc) { +func permissionScenario(t *testing.T, desc string, fn permissionScenarioFunc) { t.Helper() - guardianMock := &guardian.FakeDashboardGuardian{ - CanSaveValue: canSave, - CanViewValue: true, - } - t.Run(desc, func(t *testing.T) { features := featuremgmt.WithFeatures() cfg := setting.NewCfg() @@ -919,10 +815,11 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) dashboardService.RegisterDashboardPermissions(dashboardPermissions) require.NoError(t, err) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.NewNopLogger()) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.UID, sqlStore) @@ -942,14 +839,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc require.False(t, savedDashInFolder.IsFolder) require.NotEmpty(t, savedDashInFolder.UID) - origNewDashboardGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewDashboardGuardian - }) - guardian.MockDashboardGuardian(guardianMock) - sc := &permissionScenarioContext{ - dashboardGuardianMock: guardianMock, sqlStore: sqlStore, savedDashInFolder: savedDashInFolder, otherSavedFolder: otherSavedFolder, @@ -962,11 +852,17 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc }) } -func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) *dashboards.Dashboard { +func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB, permissions map[int64]map[string][]string) (*dashboards.Dashboard, error) { t.Helper() features := featuremgmt.WithFeatures() dto := toSaveDashboardDto(cmd) + var ac accesscontrol.AccessControl + ac = actest.FakeAccessControl{ExpectedEvaluate: true} + if permissions != nil { + dto.User = &user.SignedInUser{UserID: cmd.UserID, OrgID: testOrgID, Permissions: permissions} + ac = acimpl.ProvideAccessControl(features) + } cfg := setting.NewCfg() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) @@ -1002,7 +898,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, - actest.FakeAccessControl{}, + ac, folderService, folder.NewFakeStore(), nil, @@ -1014,64 +910,12 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) - res, err := service.SaveDashboard(context.Background(), &dto, false) - require.NoError(t, err) - - return res -} - -func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) error { - features := featuremgmt.WithFeatures() - dto := toSaveDashboardDto(cmd) - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService(folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - ) - service, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - featuremgmt.WithFeatures(), - accesscontrolmock.NewMockedPermissionsService(), - actest.FakeAccessControl{}, - folderService, - folder.NewFakeStore(), - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) - _, err = service.SaveDashboard(context.Background(), &dto, false) - return err + return service.SaveDashboard(context.Background(), &dto, false) } func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string, sqlStore db.DB) *dashboards.Dashboard { @@ -1127,7 +971,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string cfg, dashboardStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), - actest.FakeAccessControl{}, + actest.FakeAccessControl{ExpectedEvaluate: true}, folderService, folder.NewFakeStore(), nil, @@ -1139,6 +983,8 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) @@ -1206,7 +1052,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, - actest.FakeAccessControl{}, + actest.FakeAccessControl{ExpectedEvaluate: true}, folderService, folder.NewFakeStore(), nil, @@ -1218,6 +1064,8 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 4c9562e11de..82600d9f2cd 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -18,20 +18,25 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -49,6 +54,7 @@ func TestDashboardService(t *testing.T) { log: log.New("test.logger"), dashboardStore: &fakeStore, folderService: folderSvc, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, features: featuremgmt.WithFeatures(), publicDashboardService: fakePublicDashboardService, } @@ -56,10 +62,6 @@ func TestDashboardService(t *testing.T) { folderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(nil, dashboards.ErrFolderNotFound).Once() service.folderStore = &folderStore - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - t.Run("Save dashboard validation", func(t *testing.T) { dto := &dashboards.SaveDashboardDTO{} @@ -816,6 +818,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}}, }, publicDashboardService: fakePublicDashboardService, + log: log.NewNopLogger(), } t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { @@ -834,9 +837,6 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { _, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once() - fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -969,6 +969,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, }, publicDashboardService: fakePublicDashboardService, + log: log.NewNopLogger(), } ctx, k8sCliMock := setupK8sDashboardTests(singleOrgService) provisioningTimestamp := int64(1234567) @@ -1033,8 +1034,6 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { // Mock deleteDashboard() k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Once() // Mock WaitForSearchQuery() // First call returns 1 hit @@ -1055,6 +1054,37 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { require.NoError(t, err) k8sCliMock.AssertExpectations(t) }) + + t.Run("Will not wait for indexer when no dashboards were deleted", func(t *testing.T) { + repo := "test" + singleOrgService := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + orgService: &orgtest.FakeOrgService{ + ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, + }, + publicDashboardService: fakePublicDashboardService, + log: log.NewNopLogger(), + } + ctx, k8sCliMock := setupK8sDashboardTests(singleOrgService) + + // Call to searchProvisionedDashboardsThroughK8s() + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool { + // make sure the kind is added to the query + return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck + req.Options.Fields[1].Values[0] == repo + })).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{}, + TotalHits: 0, + }, nil) + + err := singleOrgService.DeleteOrphanedProvisionedDashboards(ctx, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ + ReaderNames: []string{"test"}, + }) + require.NoError(t, err) + k8sCliMock.AssertExpectations(t) + }) } func TestUnprovisionDashboard(t *testing.T) { @@ -1258,13 +1288,10 @@ func TestSetDefaultPermissionsWhenSavingFolderForProvisionedDashboards(t *testin UID: "general", }, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, log: log.NewNopLogger(), } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - cmd := &folder.CreateFolderCommand{ Title: "foo", OrgID: 1, @@ -1292,13 +1319,10 @@ func TestSaveProvisionedDashboard(t *testing.T) { UID: "general", }, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, log: log.NewNopLogger(), } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - query := &dashboards.SaveDashboardDTO{ OrgID: 1, User: &user.SignedInUser{UserID: 1}, @@ -1335,9 +1359,8 @@ func TestSaveProvisionedDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) - k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{}) @@ -1358,12 +1381,9 @@ func TestSaveDashboard(t *testing.T) { folderService: &foldertest.FakeService{ ExpectedFolder: &folder.Folder{}, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - query := &dashboards.SaveDashboardDTO{ OrgID: 1, User: &user.SignedInUser{UserID: 1}, @@ -1400,10 +1420,9 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) dashboard, err := service.SaveDashboard(ctx, query, false) require.NoError(t, err) @@ -1412,7 +1431,6 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1424,9 +1442,8 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should return an error if uid is invalid", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) query.Dashboard.UID = "invalid/uid" _, err := service.SaveDashboard(ctx, query, false) @@ -1458,8 +1475,6 @@ func TestDeleteDashboard(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() err := service.DeleteDashboard(ctx, 1, "uid", 1) require.NoError(t, err) @@ -1470,8 +1485,6 @@ func TestDeleteDashboard(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -2132,9 +2145,15 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) { service := &DashboardServiceImpl{k8sclient: k8sCliMock} query := &dashboards.FindPersistedDashboardsQuery{ OrgId: 1, + Sort: sort.SortAlphaAsc, } k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") - k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool { + return len(req.SortBy) == 1 && + // should be converted to "title" due to ParseSortName + req.SortBy[0].Field == "title" && + !req.SortBy[0].Desc + })).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ { @@ -2290,3 +2309,335 @@ func TestLegacySaveCommandToUnstructured(t *testing.T) { assert.Equal(t, result.GetAnnotations(), map[string]string(nil)) }) } + +func TestCleanUpDashboard(t *testing.T) { + tests := []struct { + name string + deleteError error + cleanupError error + expectCleanup bool + expectedError error + }{ + { + name: "Should delete public dashboards and clean up after delete", + expectCleanup: true, + }, + { + name: "Should return error if DeleteByDashboardUIDs fails", + deleteError: fmt.Errorf("deletion error"), + expectCleanup: false, + expectedError: fmt.Errorf("deletion error"), + }, + { + name: "Should return error if CleanupAfterDelete fails", + cleanupError: fmt.Errorf("cleanup error"), + expectCleanup: true, + expectedError: fmt.Errorf("cleanup error"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + publicDashboardService: fakePublicDashboardService, + } + + ctx := context.Background() + dashboardUID := "dash-uid" + orgID := int64(1) + + // Setup mocks + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, orgID, []string{dashboardUID}).Return(tc.deleteError).Maybe() + + if tc.expectCleanup { + fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{ + OrgID: orgID, + UID: dashboardUID, + }).Return(tc.cleanupError).Maybe() + } + + // Execute + err := service.CleanUpDashboard(ctx, dashboardUID, orgID) + + // Assert + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError.Error(), err.Error()) + } else { + require.NoError(t, err) + } + + fakePublicDashboardService.AssertExpectations(t) + fakeStore.AssertExpectations(t) + }) + } +} + +func TestK8sDashboardCleanupJob(t *testing.T) { + tests := []struct { + name string + featureEnabled bool + batchSize int + setupFunc func(*DashboardServiceImpl, context.Context, *client.MockK8sHandler) + verifyFunc func(*testing.T, *DashboardServiceImpl, context.Context, *client.MockK8sHandler, *kvstore.FakeKVStore) + }{ + { + name: "Should not run cleanup when feature flag is disabled", + featureEnabled: false, + batchSize: 10, + }, + { + name: "Should process dashboard cleanup for all orgs", + featureEnabled: true, + batchSize: 10, + setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) { + // Test organizations + fakeOrgService := service.orgService.(*orgtest.FakeOrgService) + fakeOrgService.ExpectedOrgs = []*org.OrgDTO{ + {ID: 1, Name: "org1"}, + {ID: 2, Name: "org2"}, + } + + kv := service.kvstore.(*kvstore.FakeKVStore) + fakeStore := service.dashboardStore.(*dashboards.FakeDashboardStore) + fakePublicDashboardService := service.publicDashboardService.(*publicdashboards.FakePublicDashboardServiceWrapper) + + // Create dashboard unstructured items for response + dashboard1 := createTestUnstructuredDashboard("dash1", "org1-dashboard", "101") + dashboard2 := createTestUnstructuredDashboard("dash2", "org2-dashboard", "201") + + // Setup test data in KV store. Only populate org 1. + _ = kv.Set(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey, "100") + + // Mock K8s responses for org 1 + k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool { + return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" && + opts.Continue == "" + })).Return(&unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "resourceVersion": "101", + }, + }, + Items: []unstructured.Unstructured{dashboard1}, + }, nil).Once() + + // Mock K8s responses for org 2 + k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(2), mock.MatchedBy(func(opts metav1.ListOptions) bool { + return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" && + opts.Continue == "" + })).Return(&unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "resourceVersion": "201", + }, + }, + Items: []unstructured.Unstructured{dashboard2}, + }, nil).Once() + + // Mock GetUserFromMeta calls + k8sCliMock.On("GetUserFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(&user.User{}, nil).Times(4) + + // Mock cleanup + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once() + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(2), []string{"dash2"}).Return(nil).Once() + fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Times(2) + }, + verifyFunc: func(t *testing.T, service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler, kv *kvstore.FakeKVStore) { + k8sCliMock.AssertExpectations(t) + + // Verify KV store was updated with new resource versions + val1, found1, _ := kv.Get(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey) + require.True(t, found1) + require.Equal(t, "101", val1) + + val2, found2, _ := kv.Get(ctx, int64(2), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey) + require.True(t, found2) + require.Equal(t, "201", val2) + }, + }, + { + name: "Should handle pagination and batching when processing large sets of dashboards", + featureEnabled: true, + batchSize: 3, + setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) { + // Test organization + fakeOrgService := service.orgService.(*orgtest.FakeOrgService) + fakeOrgService.ExpectedOrgs = []*org.OrgDTO{ + {ID: 1, Name: "org1"}, + } + + kv := service.kvstore.(*kvstore.FakeKVStore) + fakeStore := service.dashboardStore.(*dashboards.FakeDashboardStore) + fakePublicDashboardService := service.publicDashboardService.(*publicdashboards.FakePublicDashboardServiceWrapper) + + // Setup initial resource version + initialVersion := "100" + _ = kv.Set(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey, initialVersion) + + // Create dashboard batches (5 dashboards total, to be processed in 2 batches) + firstBatch := []unstructured.Unstructured{ + createTestUnstructuredDashboard("dash1", "dashboard1", "101"), + createTestUnstructuredDashboard("dash2", "dashboard2", "102"), + createTestUnstructuredDashboard("dash3", "dashboard3", "150"), + } + secondBatch := []unstructured.Unstructured{ + createTestUnstructuredDashboard("dash4", "dashboard4", "180"), + createTestUnstructuredDashboard("dash5", "dashboard5", "200"), + } + + // First batch response with continue token + k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool { + return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" && + opts.Continue == "" + })).Return(&unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "resourceVersion": "200", + "continue": "next-token", + }, + }, + Items: firstBatch, + }, nil).Once() + + // Second batch response with updated resource version + k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool { + return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" && + opts.Continue == "next-token" + })).Return(&unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "resourceVersion": "200", + }, + }, + Items: secondBatch, + }, nil).Once() + + // Mock GetUserFromMeta calls for each dashboard + k8sCliMock.On("GetUserFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(&user.User{}, nil).Times(10) + + // Mock public dashboard deletion for each dashboard + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once() + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash2"}).Return(nil).Once() + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash3"}).Return(nil).Once() + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash4"}).Return(nil).Once() + fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash5"}).Return(nil).Once() + + // Mock cleanup after delete for each dashboard + fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Times(5) + }, + verifyFunc: func(t *testing.T, service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler, kv *kvstore.FakeKVStore) { + k8sCliMock.AssertExpectations(t) + + // Verify KV store was updated with latest resource version + val, found, _ := kv.Get(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey) + require.True(t, found) + require.Equal(t, "200", val) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test database and utilities + sqlStore, _ := sqlstore.InitTestDB(t) + lockService := serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()) + kv := kvstore.NewFakeKVStore() + + fakeStore := dashboards.FakeDashboardStore{} + fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) + fakeOrgService := orgtest.NewOrgServiceFake() + + features := featuremgmt.WithFeatures() + if tc.featureEnabled { + features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) + } + + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + log: log.New("test.logger"), + dashboardStore: &fakeStore, + publicDashboardService: fakePublicDashboardService, + orgService: fakeOrgService, + serverLockService: lockService, + kvstore: kv, + features: features, + } + + ctx, k8sCliMock := setupK8sDashboardTests(service) + + if tc.setupFunc != nil { + tc.setupFunc(service, ctx, k8sCliMock) + } + + // Execute + err := service.cleanupK8sDashboardResources(ctx, int64(tc.batchSize), 20*time.Second) + require.NoError(t, err) + + if tc.verifyFunc != nil { + tc.verifyFunc(t, service, ctx, k8sCliMock, kv) + } + }) + } + + t.Run("Should start and stop background job correctly", func(t *testing.T) { + // Setup test database and utilities + sqlStore, _ := sqlstore.InitTestDB(t) + lockService := serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()) + + cfg := setting.NewCfg() + cfg.K8sDashboardCleanup = setting.K8sDashboardCleanupSettings{ + Interval: 30 * time.Second, + Timeout: 25 * time.Second, + BatchSize: 10, + } + + service := &DashboardServiceImpl{ + cfg: cfg, + log: log.New("test.logger"), + features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders), + serverLockService: lockService, + } + + // Create a test context that can be canceled + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start job with the context + done := service.startK8sDeletedDashboardsCleanupJob(ctx) + + // Cancel context to verify graceful shutdown + cancel() + + // Wait for goroutine to exit instead of using sleep + select { + case <-done: + // Job exited successfully + case <-time.After(time.Second): + t.Fatal("Cleanup job didn't exit within timeout") + } + }) +} + +// Helper functions for testing + +func createTestUnstructuredDashboard(uid, title string, resourceVersion string) unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": dashboardv0alpha1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "metadata": map[string]interface{}{ + "name": uid, + "deletionTimestamp": "2023-01-01T00:00:00Z", + "resourceVersion": resourceVersion, + }, + "spec": map[string]interface{}{ + "title": title, + }, + }, + } +} diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 9c4fedb47e4..c647528785b 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -11,6 +11,9 @@ import ( common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/infra/tracing" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" @@ -121,6 +124,8 @@ func TestValidateDashboardExists(t *testing.T) { nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) s := ProvideService(dsStore, secretsService, dashSvc) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 31103e1821a..c054154a72d 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -221,25 +221,42 @@ func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string, orgID int return result.ID, nil } -func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, rv int64) (*dashver.DashboardVersionDTO, error) { - out, err := s.k8sclient.Get(ctx, dashboardUID, orgID, v1.GetOptions{ResourceVersion: strconv.FormatInt(rv, 10)}) - if err != nil { - if apierrors.IsNotFound(err) { +func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, version int64) (*dashver.DashboardVersionDTO, error) { + // this is an unideal implementation - we have to list all versions and filter here, since there currently is no way to query for the + // generation id in unified storage, so we cannot query for the dashboard version directly, and we cannot use search as history is not indexed. + // use batches to make sure we don't load too much data at once. + const batchSize = 50 + labelSelector := utils.LabelKeyGetHistory + "=" + dashboardUID + var continueToken string + for { + out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{ + LabelSelector: labelSelector, + Limit: int64(batchSize), + Continue: continueToken, + }) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, dashboards.ErrDashboardNotFound + } + return nil, err + } + if out == nil { return nil, dashboards.ErrDashboardNotFound } - return nil, err - } - if out == nil { - return nil, dashboards.ErrDashboardNotFound + for _, item := range out.Items { + if item.GetGeneration() == version { + return s.UnstructuredToLegacyDashboardVersion(ctx, &item, orgID) + } + } + + continueToken = out.GetContinue() + if continueToken == "" || len(out.Items) == 0 { + break + } } - dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, out, orgID) - if err != nil { - return nil, err - } - - return dash, nil + return nil, dashboards.ErrDashboardNotFound } func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string) (*dashver.DashboardVersionResponse, error) { @@ -308,10 +325,9 @@ func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item createdBy = updatedBy } } - - id, err := obj.GetResourceVersionInt64() - if err != nil { - return nil, err + created := obj.GetCreationTimestamp().Time + if updated, err := obj.GetUpdatedTimestamp(); err == nil && updated != nil { + created = *updated } restoreVer, err := getRestoreVersion(obj.GetMessage()) @@ -320,10 +336,10 @@ func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item } out := dashver.DashboardVersionDTO{ - ID: id, + ID: dashVersion, DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck DashboardUID: uid, - Created: obj.GetCreationTimestamp().Time, + Created: created, CreatedBy: createdBy.ID, Message: obj.GetMessage(), RestoredFrom: restoreVer, diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index bd022a00640..4f210ea906f 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -49,8 +50,9 @@ func TestDashboardVersionService(t *testing.T) { dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) - mockCli.On("GetUserFromMeta", mock.Anything, "user:1").Return(&user.User{ID: 1}, nil) - mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(&unstructured.Unstructured{ + creationTimestamp := time.Now().Add(time.Hour * -24).UTC() + updatedTimestamp := time.Now().UTC().Truncate(time.Second) + dash := &unstructured.Unstructured{ Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -66,7 +68,14 @@ func TestDashboardVersionService(t *testing.T) { "spec": map[string]any{ "hello": "world", }, - }}, nil).Once() + }} + dash.SetCreationTimestamp(v1.NewTime(creationTimestamp)) + obj, err := utils.MetaAccessor(dash) + require.NoError(t, err) + obj.SetUpdatedTimestamp(&updatedTimestamp) + mockCli.On("GetUserFromMeta", mock.Anything, "user:1").Return(&user.User{ID: 1}, nil) + mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{*dash}}, nil).Once() res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ DashboardID: 42, OrgID: 1, @@ -74,32 +83,34 @@ func TestDashboardVersionService(t *testing.T) { }) require.Nil(t, err) require.Equal(t, res, &dashver.DashboardVersionDTO{ - ID: 12, // RV should be used + ID: 10, Version: 10, ParentVersion: 9, DashboardID: 42, DashboardUID: "uid", CreatedBy: 1, + Created: updatedTimestamp, Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10), "hello": "world"}), }) mockCli.On("GetUserFromMeta", mock.Anything, "user:2").Return(&user.User{ID: 2}, nil) - mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "11"}, mock.Anything).Return(&unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "uid", - "resourceVersion": "11", - "generation": int64(11), - "labels": map[string]any{ - utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{{ + Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "11", + "generation": int64(11), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + "annotations": map[string]any{ + utils.AnnoKeyCreatedBy: "user:1", + utils.AnnoKeyUpdatedBy: "user:2", // if updated by is set, that is the version creator + }, }, - "annotations": map[string]any{ - utils.AnnoKeyCreatedBy: "user:1", - utils.AnnoKeyUpdatedBy: "user:2", // if updated by is set, that is the version creator - }, - }, - "spec": map[string]any{}, - }}, nil).Once() + "spec": map[string]any{}, + }}}}, nil).Once() res, err = dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ DashboardID: 42, OrgID: 1, @@ -107,7 +118,7 @@ func TestDashboardVersionService(t *testing.T) { }) require.Nil(t, err) require.Equal(t, res, &dashver.DashboardVersionDTO{ - ID: 11, // RV should be used + ID: 11, Version: 11, ParentVersion: 10, DashboardID: 42, @@ -124,7 +135,7 @@ func TestDashboardVersionService(t *testing.T) { dashboardVersionService.k8sclient = mockCli dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) - mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid")) + mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid")) _, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ DashboardID: 42, @@ -276,7 +287,7 @@ func TestListDashboardVersions(t *testing.T) { require.Equal(t, 1, len(res.Versions)) require.EqualValues(t, &dashver.DashboardVersionResponse{ Versions: []*dashver.DashboardVersionDTO{{ - ID: 12, // should take rv + ID: 5, DashboardID: 42, ParentVersion: 4, Version: 5, // should take from spec diff --git a/pkg/services/datasources/fakes/fake_datasource_service.go b/pkg/services/datasources/fakes/fake_datasource_service.go index 43a71852c43..c23024b4e5b 100644 --- a/pkg/services/datasources/fakes/fake_datasource_service.go +++ b/pkg/services/datasources/fakes/fake_datasource_service.go @@ -122,11 +122,7 @@ func (s *FakeDataSourceService) GetHTTPTransport(ctx context.Context, ds *dataso } func (s *FakeDataSourceService) DecryptedValues(ctx context.Context, ds *datasources.DataSource) (map[string]string, error) { - if s.SimulatePluginFailure { - return nil, datasources.ErrDatasourceSecretsPluginUserFriendly{Err: "unknown error"} - } - values := make(map[string]string) - return values, nil + return make(map[string]string), nil } func (s *FakeDataSourceService) DecryptedValue(ctx context.Context, ds *datasources.DataSource, key string) (string, bool, error) { diff --git a/pkg/services/datasources/models.go b/pkg/services/datasources/models.go index 84592aadda6..eb4f51f389c 100644 --- a/pkg/services/datasources/models.go +++ b/pkg/services/datasources/models.go @@ -145,15 +145,6 @@ func (ds DataSource) AllowedCookies() []string { return []string{} } -// Specific error type for grpc secrets management so that we can show more detailed plugin errors to users -type ErrDatasourceSecretsPluginUserFriendly struct { - Err string -} - -func (e ErrDatasourceSecretsPluginUserFriendly) Error() string { - return e.Err -} - // ---------------------- // COMMANDS diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 33cd628c9db..89f2d6f8220 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -376,7 +376,7 @@ var ( { Name: "frontendSandboxMonitorOnly", Description: "Enables monitor only in the plugin frontend sandbox (if enabled)", - Stage: FeatureStageExperimental, + Stage: FeatureStagePrivatePreview, FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, @@ -441,13 +441,6 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, Expression: "false", }, - { - Name: "metricsSummary", - Description: "Enables metrics summary queries in the Tempo data source", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaObservabilityTracesAndProfilingSquad, - }, { Name: "datasourceAPIServers", Description: "Expose some datasources as apiservers.", @@ -656,12 +649,6 @@ var ( Owner: grafanaAppPlatformSquad, FrontendOnly: true, }, - { - Name: "kubernetesRestore", - Description: "Allow restoring objects in k8s", - Stage: FeatureStageExperimental, - Owner: grafanaAppPlatformSquad, - }, { Name: "kubernetesClientDashboardsFolders", Description: "Route the folder and dashboard service requests to k8s", @@ -951,6 +938,12 @@ var ( Owner: grafanaOperatorExperienceSquad, Expression: "true", }, + { + Name: "secretsManagementAppPlatform", + Description: "Enable the secrets management API and services under app platform", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + }, { Name: "alertingSaveStatePeriodic", Description: "Writes the state periodically to the database, asynchronous to rule evaluation", @@ -1399,8 +1392,9 @@ var ( { Name: "unifiedStorageSearchPermissionFiltering", Description: "Enable permission filtering on unified storage search", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaSearchAndStorageSquad, + Expression: "true", HideFromDocs: true, HideFromAdminPage: true, }, @@ -1415,8 +1409,9 @@ var ( { Name: "pluginsSriChecks", Description: "Enables SRI checks for plugin assets", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaPluginsPlatformSquad, + Expression: "false", // disabled by default }, { Name: "unifiedStorageBigObjectsSupport", @@ -1829,6 +1824,14 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "unifiedStorageGrpcConnectionPool", + Description: "Enables the unified storage grpc connection pool", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromAdminPage: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 0cdaace5e3e..cb90956b908 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -413,7 +413,6 @@ unifiedStorageSearchUI,2024-12-19T18:21:48Z,,a8f347144ddc16f2033fdeb4f3474e49239 playlistsReconciler,2024-12-20T03:09:31Z,,24bf337c562dc9b9d8684cc9acb7ea171ea83414,Charandas k8SFolderCounts,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira k8SFolderMove,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira -kubernetesRestore,2025-01-03T14:48:47Z,,5429512779bd5f25b88ff728ea91efdef7dfafa0,Stephanie Hingtgen improvedExternalSessionHandlingSAML,2025-01-09T17:02:49Z,,c52ec21c75ab72c2f7d28259bac0364edae560d0,Misi teamHttpHeadersMimir,2025-01-13T10:42:47Z,,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck ABTestFeatureToggleA,2025-01-13T21:13:13Z,,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0b2cded925a..62bd4844236 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -49,7 +49,7 @@ enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true pluginsFrontendSandbox,privatePreview,@grafana/plugins-platform-backend,false,false,false -frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true +frontendSandboxMonitorOnly,privatePreview,@grafana/plugins-platform-backend,false,false,true pluginsDetailsRightPanel,privatePreview,@grafana/plugins-platform-backend,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false @@ -58,7 +58,6 @@ awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false, transformationsRedesign,GA,@grafana/observability-metrics,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false traceQLStreaming,GA,@grafana/observability-traces-and-profiling,false,false,true -metricsSummary,experimental,@grafana/observability-traces-and-profiling,false,false,true datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -86,7 +85,6 @@ formatString,GA,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true -kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false kubernetesClientDashboardsFolders,experimental,@grafana/grafana-app-platform-squad,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -125,6 +123,7 @@ alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false,false,false +secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false @@ -183,9 +182,9 @@ useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,fal rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false -unifiedStorageSearchPermissionFiltering,experimental,@grafana/search-and-storage,false,false,false +unifiedStorageSearchPermissionFiltering,GA,@grafana/search-and-storage,false,false,false managedDualWriter,experimental,@grafana/search-and-storage,false,false,false -pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,false +pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false prometheusUsesCombobox,experimental,@grafana/oss-big-tent,false,false,false @@ -241,3 +240,4 @@ extraLanguages,experimental,@grafana/grafana-frontend-platform,false,false,true noBackdropBlur,experimental,@grafana/grafana-frontend-platform,false,false,true alertingMigrationUI,experimental,@grafana/alerting-squad,false,false,true unifiedStorageHistoryPruner,experimental,@grafana/search-and-storage,false,false,false +unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 1f4dee5e21d..b1d5e756e5b 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -243,10 +243,6 @@ const ( // Enables response streaming of TraceQL queries of the Tempo data source FlagTraceQLStreaming = "traceQLStreaming" - // FlagMetricsSummary - // Enables metrics summary queries in the Tempo data source - FlagMetricsSummary = "metricsSummary" - // FlagDatasourceAPIServers // Expose some datasources as apiservers. FlagDatasourceAPIServers = "datasourceAPIServers" @@ -355,10 +351,6 @@ const ( // Use the kubernetes API in the frontend for dashboards FlagKubernetesDashboards = "kubernetesDashboards" - // FlagKubernetesRestore - // Allow restoring objects in k8s - FlagKubernetesRestore = "kubernetesRestore" - // FlagKubernetesClientDashboardsFolders // Route the folder and dashboard service requests to k8s FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders" @@ -511,6 +503,10 @@ const ( // Enable the Grafana Migration Assistant, which helps you easily migrate on-prem resources, such as dashboards, folders, and data source configurations, to your Grafana Cloud stack. FlagOnPremToCloudMigrations = "onPremToCloudMigrations" + // FlagSecretsManagementAppPlatform + // Enable the secrets management API and services under app platform + FlagSecretsManagementAppPlatform = "secretsManagementAppPlatform" + // FlagAlertingSaveStatePeriodic // Writes the state periodically to the database, asynchronous to rule evaluation FlagAlertingSaveStatePeriodic = "alertingSaveStatePeriodic" @@ -974,4 +970,8 @@ const ( // FlagUnifiedStorageHistoryPruner // Enables the unified storage history pruner FlagUnifiedStorageHistoryPruner = "unifiedStorageHistoryPruner" + + // FlagUnifiedStorageGrpcConnectionPool + // Enables the unified storage grpc connection pool + FlagUnifiedStorageGrpcConnectionPool = "unifiedStorageGrpcConnectionPool" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 64036ecda10..1d0e357c378 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1913,12 +1913,15 @@ { "metadata": { "name": "frontendSandboxMonitorOnly", - "resourceVersion": "1718727528075", - "creationTimestamp": "2023-07-05T11:48:25Z" + "resourceVersion": "1741965568775", + "creationTimestamp": "2023-07-05T11:48:25Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-03-14 15:19:28.77575 +0000 UTC" + } }, "spec": { "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", - "stage": "experimental", + "stage": "privatePreview", "codeowner": "@grafana/plugins-platform-backend", "frontend": true } @@ -2401,18 +2404,6 @@ "expression": "true" } }, - { - "metadata": { - "name": "kubernetesRestore", - "resourceVersion": "1735880498698", - "creationTimestamp": "2025-01-03T14:48:47Z" - }, - "spec": { - "description": "Allow restoring objects in k8s", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad" - } - }, { "metadata": { "name": "kubernetesSnapshots", @@ -2777,7 +2768,8 @@ "metadata": { "name": "metricsSummary", "resourceVersion": "1718727528075", - "creationTimestamp": "2023-08-28T14:02:12Z" + "creationTimestamp": "2023-08-28T14:02:12Z", + "deletionTimestamp": "2024-11-25T10:47:18Z" }, "spec": { "description": "Enables metrics summary queries in the Tempo data source", @@ -3281,13 +3273,17 @@ { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1727785264632", - "creationTimestamp": "2024-10-04T12:55:09Z" + "resourceVersion": "1742405194835", + "creationTimestamp": "2024-10-04T12:55:09Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-03-19 17:26:34.83562 +0000 UTC" + } }, "spec": { "description": "Enables SRI checks for plugin assets", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "stage": "GA", + "codeowner": "@grafana/plugins-platform-backend", + "expression": "false" } }, { @@ -3819,6 +3815,18 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "secretsManagementAppPlatform", + "resourceVersion": "1742370596238", + "creationTimestamp": "2025-03-19T07:49:56Z" + }, + "spec": { + "description": "Enable the secrets management API and services under app platform", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" + } + }, { "metadata": { "name": "showDashboardValidationWarnings", @@ -4196,6 +4204,20 @@ "codeowner": "@grafana/search-and-storage" } }, + { + "metadata": { + "name": "unifiedStorageGrpcConnectionPool", + "resourceVersion": "1742549790491", + "creationTimestamp": "2025-03-21T09:36:30Z" + }, + "spec": { + "description": "Enables the unified storage grpc connection pool", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "unifiedStorageHistoryPruner", @@ -4242,15 +4264,19 @@ { "metadata": { "name": "unifiedStorageSearchPermissionFiltering", - "resourceVersion": "1737489629408", - "creationTimestamp": "2025-01-22T11:38:37Z" + "resourceVersion": "1742564039800", + "creationTimestamp": "2025-01-22T11:38:37Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-03-21 13:33:59.800619 +0000 UTC" + } }, "spec": { "description": "Enable permission filtering on unified storage search", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/search-and-storage", "hideFromAdminPage": true, - "hideFromDocs": true + "hideFromDocs": true, + "expression": "true" } }, { diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index b6361936a5d..fcb88a922cf 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -20,8 +20,10 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -498,7 +500,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, - client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), + ) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -584,7 +589,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, - folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), + ) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) @@ -729,7 +737,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, - dualwrite.ProvideTestService(), sort.ProvideService()) + dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), + ) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -1524,6 +1535,8 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 14601344a82..20c17c324b5 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -607,9 +607,9 @@ func (ss *FolderStoreImpl) GetDescendants(ctx context.Context, orgID int64, ance } func getFullpathSQL(dialect migrator.Dialect) string { - escaped := "\\/" - if dialect.DriverName() == migrator.MySQL { - escaped = "\\\\/" + escaped := `\/` + if dialect.DriverName() == migrator.MySQL || dialect.DriverName() == migrator.Spanner { + escaped = `\\/` } concatCols := make([]string, 0, folder.MaxNestedFolderDepth) concatCols = append(concatCols, fmt.Sprintf("COALESCE(REPLACE(f0.title, '/', '%s'), '')", escaped)) diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 7100b7ffacc..58ebe2aa878 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" ) @@ -34,12 +33,6 @@ var New = func(ctx context.Context, dashId int64, orgId int64, user identity.Req panic("no guardian factory implementation provided") } -// NewByDashboard factory for creating a new dashboard guardian instance -// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned -var NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { - panic("no guardian factory implementation provided") -} - // NewByFolderUID factory for creating a new folder guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned var NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { @@ -108,13 +101,6 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { mock.User = user return mock, nil } - NewByDashboard = func(_ context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { - mock.OrgID = orgId - mock.DashUID = dash.UID - mock.DashID = dash.ID - mock.User = user - return mock, nil - } NewByFolderUID = func(_ context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { mock.OrgID = orgId diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index 7e3902f1e5a..7be96481b51 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -31,10 +31,6 @@ func InitAccessControlGuardian( return NewAccessControlDashboardGuardian(ctx, cfg, dashId, user, ac, dashboardService, folderService, logger) } - NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardianByDashboard(ctx, cfg, dash, user, ac, dashboardService, folderService, logger) - } - NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { return NewAccessControlFolderGuardianByUID(ctx, cfg, folderUID, user, ac, dashboardService, folderService) } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index ee1d1fe7ec6..8eec5685b14 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -18,7 +18,9 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -366,6 +368,8 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) @@ -459,6 +463,8 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena features, folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, svcErr) dashboardService.RegisterDashboardPermissions(dashboardPermissions) @@ -532,6 +538,8 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo features, folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, dashSvcErr) dashService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 0828e9578f6..0189d785fd5 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -26,7 +26,7 @@ type LibraryElement struct { Kind int64 Type string Description string - Model json.RawMessage + Model json.RawMessage `xorm:"TEXT"` // Column is defined as TEXT in `library_element`. Version int64 Created time.Time diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 531f3714eca..930ec705e64 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -15,7 +15,9 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" @@ -739,7 +741,8 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash features, acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - ) + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) service.RegisterDashboardPermissions(dashPermissionService) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -837,7 +840,8 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo features, acmock.NewMockedPermissionsService(), ac, folderSvc, folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - ) + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) guardian.InitAccessControlGuardian(cfg, ac, dashService, folderSvc, log.NewNopLogger()) diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index 2390f409fb4..286dad7c49c 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/live/model" ) @@ -63,6 +63,7 @@ type DashboardHandler struct { ClientCount model.ChannelClientCount Store db.DB DashboardService dashboards.DashboardService + AccessControl accesscontrol.AccessControl } // GetHandlerForPath called on init @@ -77,20 +78,17 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user identity.Reques // make sure can view this dashboard if len(parts) == 2 && parts[0] == "uid" { query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.GetOrgID()} - queryResult, err := h.DashboardService.GetDashboard(ctx, &query) + _, err := h.DashboardService.GetDashboard(ctx, &query) if err != nil { logger.Error("Error getting dashboard", "query", query, "error", err) return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil } - dash := queryResult - guard, err := guardian.NewByDashboard(ctx, dash, user.GetOrgID(), user) - if err != nil { + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1])) + canView, err := h.AccessControl.Evaluate(ctx, user, evaluator) + if err != nil || !canView { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err } - if canView, err := guard.CanView(); err != nil || !canView { - return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil - } return model.SubscribeReply{ Presence: true, @@ -119,19 +117,14 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Req return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") } query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: requester.GetOrgID()} - queryResult, err := h.DashboardService.GetDashboard(ctx, &query) + _, err = h.DashboardService.GetDashboard(ctx, &query) if err != nil { logger.Error("Unknown dashboard", "query", query) return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil } - guard, err := guardian.NewByDashboard(ctx, queryResult, requester.GetOrgID(), requester) - if err != nil { - logger.Error("Failed to create guardian", "err", err) - return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") - } - - canEdit, err := guard.CanEdit() + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1])) + canEdit, err := h.AccessControl.Evaluate(ctx, requester, evaluator) if err != nil { return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") } diff --git a/pkg/services/live/features/watch.go b/pkg/services/live/features/watch.go new file mode 100644 index 00000000000..05a7f875518 --- /dev/null +++ b/pkg/services/live/features/watch.go @@ -0,0 +1,214 @@ +package features + +import ( + "context" + "fmt" + "strings" + "sync" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + "github.com/grafana/grafana-plugin-sdk-go/live" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/live/model" +) + +// WatchRunner will start a watch task and broadcast results +type WatchRunner struct { + publisher model.ChannelPublisher + configProvider apiserver.RestConfigProvider + + watchingMu sync.Mutex + watching map[string]*watcher +} + +func NewWatchRunner(publisher model.ChannelPublisher, configProvider apiserver.RestConfigProvider) *WatchRunner { + return &WatchRunner{ + publisher: publisher, + configProvider: configProvider, + watching: make(map[string]*watcher), + } +} + +func (b *WatchRunner) GetHandlerForPath(_ string) (model.ChannelHandler, error) { + return b, nil // all dashboards share the same handler +} + +// Valid paths look like: {version}/{resource}[={name}]/{user.uid} +// * v0alpha1/dashboards/u12345 +// * v0alpha1/dashboards=ABCD/u12345 +func (b *WatchRunner) OnSubscribe(ctx context.Context, u identity.Requester, e model.SubscribeEvent) (model.SubscribeReply, backend.SubscribeStreamStatus, error) { + // To make sure we do not share resources across users, in clude the UID in the path + userID := u.GetIdentifier() + if userID == "" { + return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("missing user identity") + } + if !strings.HasSuffix(e.Path, userID) { + return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("path must end with user uid (%s)", userID) + } + + // While testing with provisioning repositories, we will limit this to admin only + if !u.HasRole(identity.RoleAdmin) { + return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("only admin users for now") + } + + b.watchingMu.Lock() + defer b.watchingMu.Unlock() + + current, ok := b.watching[e.Channel] + if ok && !current.done { + return model.SubscribeReply{ + JoinLeave: false, + Presence: false, + Recover: false, + }, backend.SubscribeStreamStatusOK, nil + } + + // Try to start a watcher for this request + gvr, name, err := parseWatchRequest(e.Channel, userID) + if err != nil { + return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err + } + + // Test this with only provisiong support -- then we can evaluate a broader rollout + if gvr.Group != provisioning.GROUP { + return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, + fmt.Errorf("watching provisioned resources is OK allowed (for now)") + } + + requester := types.WithAuthInfo(context.Background(), u) + cfg, err := b.configProvider.GetRestConfig(requester) + if err != nil { + return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err + } + uclient, err := dynamic.NewForConfig(cfg) + if err != nil { + return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err + } + client := uclient.Resource(gvr).Namespace(u.GetNamespace()) + + opts := v1.ListOptions{} + if len(name) > 1 { + opts.FieldSelector = "metadata.name=" + name + } + watch, err := client.Watch(requester, opts) + if err != nil { + return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err + } + + current = &watcher{ + orgId: u.GetOrgID(), + channel: e.Channel, + publisher: b.publisher, + watch: watch, + } + + b.watching[e.Channel] = current + go current.run(ctx) + + return model.SubscribeReply{ + JoinLeave: false, // need unsubscribe envents + Presence: false, + Recover: false, + }, backend.SubscribeStreamStatusOK, nil +} + +func parseWatchRequest(channel string, user string) (gvr schema.GroupVersionResource, name string, err error) { + addr, err := live.ParseChannel(channel) + if err != nil { + return gvr, "", err + } + + parts := strings.Split(addr.Path, "/") + if len(parts) != 3 { + return gvr, "", fmt.Errorf("expecting path: {version}/{resource}={name}/{user}") + } + if parts[2] != user { + return gvr, "", fmt.Errorf("expecting user suffix: %s", user) + } + + resource := strings.Split(parts[1], "=") + gvr = schema.GroupVersionResource{ + Group: addr.Namespace, + Version: parts[0], + Resource: resource[0], + } + if len(resource) > 1 { + name = resource[1] + } + return gvr, name, nil +} + +// OnPublish is called when a client wants to broadcast on the websocket +func (b *WatchRunner) OnPublish(_ context.Context, u identity.Requester, e model.PublishEvent) (model.PublishReply, backend.PublishStreamStatus, error) { + return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("watch does not support publish") +} + +type watcher struct { + orgId int64 + channel string + publisher model.ChannelPublisher + done bool + watch watch.Interface +} + +func (b *watcher) run(ctx context.Context) { + logger := logging.FromContext(ctx).With("channel", b.channel) + + ch := b.watch.ResultChan() + for { + select { + // This is sent when there are no longer any subscriptions + case <-ctx.Done(): + logger.Info("context done", "channel", b.channel) + b.watch.Stop() + b.done = true + return + + // Each watch event + case event, ok := <-ch: + if !ok { + logger.Info("watch stream broken", "channel", b.channel) + b.watch.Stop() + b.done = true // will force reconnect from the frontend + return + } + + cfg := jsoniter.ConfigCompatibleWithStandardLibrary + stream := cfg.BorrowStream(nil) + defer cfg.ReturnStream(stream) + + // regular json.Marshal() uses upper case + stream.WriteObjectStart() + stream.WriteObjectField("type") + stream.WriteString(string(event.Type)) + stream.WriteMore() + stream.WriteObjectField("object") + stream.WriteVal(event.Object) + stream.WriteObjectEnd() + + buf := stream.Buffer() + data := make([]byte, len(buf)) + copy(data, buf) + + err := b.publisher(b.orgId, b.channel, data) + if err != nil { + logger.Error("publish error", "channel", b.channel, "err", err) + b.watch.Stop() + b.done = true // will force reconnect from the frontend + continue + } + } + } +} diff --git a/pkg/services/live/features/watch_test.go b/pkg/services/live/features/watch_test.go new file mode 100644 index 00000000000..fc82966b18a --- /dev/null +++ b/pkg/services/live/features/watch_test.go @@ -0,0 +1,67 @@ +package features + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestParseWatchRqeust(t *testing.T) { + userid := "userid" // dummy + tests := []struct { + testCase string + channel string + userid string // override + + // Expect + gvr schema.GroupVersionResource + name string + err bool + }{ + { + testCase: "dashbaords", + channel: "watch/dashboard.grafana.app/v0alpha1/dashboards/userid", + gvr: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + }, + { + testCase: "dashbaords with anme", + channel: "watch/dashboard.grafana.app/v0alpha1/dashboards=abc/userid", + gvr: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + name: "abc", + }, + { + testCase: "bad user id", + channel: "watch/dashboard.grafana.app/v0alpha1/dashboards/x", + err: true, // bad user id + }, + } + for _, tt := range tests { + t.Run(tt.testCase, func(t *testing.T) { + gvr, name, err := parseWatchRequest(tt.channel, first(tt.userid, userid)) + if tt.err { + require.Error(t, err) + return + } + require.Equal(t, tt.gvr, gvr) + require.Equal(t, tt.name, name) + }) + } +} + +func first(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 35c2be26d69..f7576c5dd6f 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -36,6 +36,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/apiserver" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -79,7 +80,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r dataSourceCache datasources.CacheService, sqlStore db.DB, secretsService secrets.Service, usageStatsService usagestats.Service, queryDataService query.Service, toggles featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService, annotationsRepo annotations.Repository, - orgService org.Service) (*GrafanaLive, error) { + orgService org.Service, configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) { g := &GrafanaLive{ Cfg: cfg, Features: toggles, @@ -184,12 +185,18 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r ClientCount: g.ClientCount, Store: sqlStore, DashboardService: dashboardService, + AccessControl: accessControl, } g.storage = database.NewStorage(g.SQLStore, g.CacheService) g.GrafanaScope.Dashboards = dash g.GrafanaScope.Features["dashboard"] = dash g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage) + // Testing watch with just the provisioning support -- this will be removed when it is well validated + if toggles.IsEnabledGlobally(featuremgmt.FlagProvisioning) { + g.GrafanaScope.Features["watch"] = features.NewWatchRunner(g.Publish, configProvider) + } + g.surveyCaller = survey.NewCaller(managedStreamRunner, node) err = g.surveyCaller.SetupHandlers() if err != nil { @@ -888,6 +895,8 @@ func (g *GrafanaLive) GetChannelHandlerFactory(ctx context.Context, user identit switch scope { case live.ScopeGrafana: return g.handleGrafanaScope(user, namespace) + case live.ScopeWatch: + return g.handleWatchScope() case live.ScopePlugin: return g.handlePluginScope(ctx, user, namespace) case live.ScopeDatasource: @@ -906,6 +915,13 @@ func (g *GrafanaLive) handleGrafanaScope(_ identity.Requester, namespace string) return nil, fmt.Errorf("unknown feature: %q", namespace) } +func (g *GrafanaLive) handleWatchScope() (model.ChannelHandlerFactory, error) { + if p, ok := g.GrafanaScope.Features["watch"]; ok { + return p, nil + } + return nil, fmt.Errorf("watch not registered") +} + func (g *GrafanaLive) handlePluginScope(ctx context.Context, _ identity.Requester, namespace string) (model.ChannelHandlerFactory, error) { streamHandler, err := g.getStreamPlugin(ctx, namespace) if err != nil { diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index 447910e1b84..c002ec7c705 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -36,7 +36,11 @@ func Test_provideLiveService_RedisUnavailable(t *testing.T) { nil, &usagestats.UsageStatsMock{T: t}, nil, - featuremgmt.WithFeatures(), acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), &dashboards.FakeDashboardService{}, annotationstest.NewFakeAnnotationsRepo(), nil) + featuremgmt.WithFeatures(), + acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), + &dashboards.FakeDashboardService{}, + annotationstest.NewFakeAnnotationsRepo(), + nil, nil) // Proceeds without live HA if redis is unavaialble require.NoError(t, err) diff --git a/pkg/services/login/authinfoimpl/store.go b/pkg/services/login/authinfoimpl/store.go index b5a90fc2a8a..84c405bf276 100644 --- a/pkg/services/login/authinfoimpl/store.go +++ b/pkg/services/login/authinfoimpl/store.go @@ -107,10 +107,11 @@ func (s *Store) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuer func (s *Store) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { authUser := &login.UserAuth{ - UserId: cmd.UserId, - AuthModule: cmd.AuthModule, - AuthId: cmd.AuthId, - Created: GetTime(), + UserId: cmd.UserId, + AuthModule: cmd.AuthModule, + AuthId: cmd.AuthId, + ExternalUID: cmd.ExternalUID, + Created: GetTime(), } if cmd.OAuthToken != nil { diff --git a/pkg/services/login/model.go b/pkg/services/login/model.go index 3d755a80301..979a89d6fb2 100644 --- a/pkg/services/login/model.go +++ b/pkg/services/login/model.go @@ -23,6 +23,7 @@ type UserAuth struct { OAuthIdToken string OAuthTokenType string OAuthExpiry time.Time + ExternalUID string `xorm:"external_uid"` } type ExternalUserInfo struct { @@ -72,10 +73,11 @@ type RequestURIKey struct{} // COMMANDS type SetAuthInfoCommand struct { - AuthModule string - AuthId string - UserId int64 - OAuthToken *oauth2.Token + AuthModule string + AuthId string + UserId int64 + OAuthToken *oauth2.Token + ExternalUID string } type UpdateAuthInfoCommand struct { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index facf246f171..e859596520f 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -23,6 +23,8 @@ import ( "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/setting" + + "github.com/open-feature/go-sdk/openfeature" ) type ServiceImpl struct { @@ -34,6 +36,7 @@ type ServiceImpl struct { pluginSettings pluginsettings.Service starService star.Service features featuremgmt.FeatureToggles + openFeature *featuremgmt.OpenFeatureService dashboardService dashboards.DashboardService accesscontrolService ac.Service kvStore kvstore.KVStore @@ -54,7 +57,7 @@ type NavigationAppConfig struct { func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service, features featuremgmt.FeatureToggles, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, - license licensing.Licensing, authnService authn.Service) navtree.Service { + license licensing.Licensing, authnService authn.Service, openFeature *featuremgmt.OpenFeatureService) navtree.Service { service := &ServiceImpl{ cfg: cfg, log: log.New("navtree service"), @@ -64,6 +67,7 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor pluginSettings: pluginSettings, starService: starService, features: features, + openFeature: openFeature, dashboardService: dashboardService, accesscontrolService: accesscontrolService, kvStore: kvStore, @@ -80,6 +84,7 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Preference) (*navtree.NavTreeRoot, error) { hasAccess := ac.HasAccess(s.accessControl, c) treeRoot := &navtree.NavTreeRoot{} + ctx := c.Req.Context() treeRoot.AddSection(s.getHomeNode(c, prefs)) @@ -184,7 +189,8 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere treeRoot.RemoveSectionByID(navtree.NavIDCfg) } - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) && c.IsSignedIn { + enabled := s.openFeature.Client.Boolean(ctx, featuremgmt.FlagPinNavItems, true, openfeature.TransactionContext(ctx)) + if enabled && c.IsSignedIn { treeRoot.AddSection(&navtree.NavLink{ Text: "Bookmarks", Id: navtree.NavIDBookmarks, diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index d9f28509622..5cbe398b459 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -463,6 +463,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( IsPaused: pauseAlertRules, }, KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition), + EvaluationOffset: &srv.cfg.PrometheusConversion.RuleQueryOffset, }, ) if err != nil { diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 437ec3ffef3..720bde8349b 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -2,7 +2,6 @@ package api import ( "context" - "fmt" "net/http" "net/http/httptest" "testing" @@ -123,15 +122,13 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { expectedRules := make(map[string]string) for _, rule := range simpleGroup.Rules { if rule.Alert != "" { - title := fmt.Sprintf("[%s] %s", simpleGroup.Name, rule.Alert) promRuleYAML, err := yaml.Marshal(rule) require.NoError(t, err) - expectedRules[title] = string(promRuleYAML) + expectedRules[rule.Alert] = string(promRuleYAML) } else if rule.Record != "" { - title := fmt.Sprintf("[%s] %s", simpleGroup.Name, rule.Record) promRuleYAML, err := yaml.Marshal(rule) require.NoError(t, err) - expectedRules[title] = string(promRuleYAML) + expectedRules[rule.Record] = string(promRuleYAML) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 44e2fd9a935..f21050c0501 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -8,6 +8,8 @@ import ( "regexp" "strings" + alertmanager_config "github.com/prometheus/alertmanager/config" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" @@ -21,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" - alertmanager_config "github.com/prometheus/alertmanager/config" ) const disableProvenanceHeaderName = "X-Disable-Provenance" @@ -357,9 +358,6 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de return ErrResp(http.StatusBadRequest, err, "") } if err != nil { - if errors.Is(err, alerting_models.ErrAlertRuleUniqueConstraintViolation) { - return ErrResp(http.StatusBadRequest, err, "") - } if errors.Is(err, store.ErrOptimisticLock) { return ErrResp(http.StatusConflict, err, "") } @@ -396,9 +394,6 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def updated.UID = UID provenance := determineProvenance(c) updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), c.SignedInUser, updated, alerting_models.Provenance(provenance)) - if errors.Is(err, alerting_models.ErrAlertRuleUniqueConstraintViolation) { - return ErrResp(http.StatusBadRequest, err, "") - } if errors.Is(err, alerting_models.ErrAlertRuleNotFound) { return response.Empty(http.StatusNotFound) } @@ -514,9 +509,6 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a } provenance := determineProvenance(c) err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance)) - if errors.Is(err, alerting_models.ErrAlertRuleUniqueConstraintViolation) { - return ErrResp(http.StatusBadRequest, err, "") - } if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) { return ErrResp(http.StatusBadRequest, err, "") } diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index d48aa5444b9..9862c862b9a 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -1,15 +1,20 @@ package api import ( + "encoding/json" "io" + "mime" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/errutil" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) +var errorUnsupportedMediaType = errutil.UnsupportedMediaType("alerting.unsupportedMediaType") + type ConvertPrometheusApiHandler struct { svc *ConvertPrometheusSrv } @@ -49,8 +54,30 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup( defer func() { _ = ctx.Req.Body.Close() }() var promGroup apimodels.PrometheusRuleGroup - if err := yaml.Unmarshal(body, &promGroup); err != nil { - return errorToResponse(err) + var m string + + // Parse content-type only if it's not empty, + // otherwise we'll assume it's yaml + contentType := ctx.Req.Header.Get("content-type") + if contentType != "" { + m, _, err = mime.ParseMediaType(contentType) + if err != nil { + return errorToResponse(err) + } + } + + switch m { + case "application/yaml", "": + // mimirtool does not send content-type, so if it's empty, we assume it's yaml + if err := yaml.Unmarshal(body, &promGroup); err != nil { + return errorToResponse(err) + } + case "application/json": + if err := json.Unmarshal(body, &promGroup); err != nil { + return errorToResponse(err) + } + default: + return errorToResponse(errorUnsupportedMediaType.Errorf("unsupported media type: %s, only application/yaml and application/json are supported", m)) } return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 49ab21484b0..12bee7c6447 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -208,6 +208,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "additionalProperties": { "type": "string" @@ -468,6 +471,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "format": "double", + "type": "number" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -3012,9 +3019,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Limit": { + "format": "int64", + "type": "integer" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "items": { "$ref": "#/definitions/PrometheusRule" @@ -3117,6 +3137,10 @@ "example": false, "type": "boolean" }, + "keep_firing_for": { + "format": "duration", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" @@ -4952,7 +4976,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 5a9c6a5b868..23cfdae71d8 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -7,7 +7,6 @@ import ( "time" "github.com/go-openapi/strfmt" - "github.com/mohae/deepcopy" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/config" "github.com/prometheus/common/model" @@ -698,26 +697,6 @@ func (c *PostableUserConfig) validate() error { return nil } -// Decrypt returns a copy of the configuration struct with decrypted secure settings in receivers. -func (c *PostableUserConfig) Decrypt(decryptFn func(payload []byte) ([]byte, error)) (PostableUserConfig, error) { - newCfg, ok := deepcopy.Copy(c).(*PostableUserConfig) - if !ok { - return PostableUserConfig{}, fmt.Errorf("failed to copy config") - } - - // Iterate through receivers and decrypt secure settings. - for _, rcv := range newCfg.AlertmanagerConfig.Receivers { - for _, gmr := range rcv.PostableGrafanaReceivers.GrafanaManagedReceivers { - decrypted, err := gmr.DecryptSecureSettings(decryptFn) - if err != nil { - return PostableUserConfig{}, err - } - gmr.SecureSettings = decrypted - } - } - return *newCfg, nil -} - // GetGrafanaReceiverMap returns a map that associates UUIDs to grafana receivers func (c *PostableUserConfig) GetGrafanaReceiverMap() map[string]*PostableGrafanaReceiver { UIDs := make(map[string]*PostableGrafanaReceiver) diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index f0202486e46..6e9f0f2bd4c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -192,9 +192,12 @@ type PrometheusNamespace struct { // swagger:model type PrometheusRuleGroup struct { - Name string `yaml:"name"` - Interval model.Duration `yaml:"interval"` - Rules []PrometheusRule `yaml:"rules"` + Name string `yaml:"name"` + Interval model.Duration `yaml:"interval"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty"` + Limit int `yaml:"limit,omitempty"` + Rules []PrometheusRule `yaml:"rules"` + Labels map[string]string `yaml:"labels,omitempty"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 3c9cc03dbec..5968f49fdbc 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -208,6 +208,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "additionalProperties": { "type": "string" @@ -468,6 +471,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "format": "double", + "type": "number" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -3012,9 +3019,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Limit": { + "format": "int64", + "type": "integer" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "items": { "$ref": "#/definitions/PrometheusRule" @@ -3117,6 +3137,10 @@ "example": false, "type": "boolean" }, + "keep_firing_for": { + "format": "duration", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 15d8a288303..706a40d67fd 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -4433,6 +4433,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -4703,6 +4706,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "type": "number", + "format": "double" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -7238,9 +7245,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Limit": { + "type": "integer", + "format": "int64" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "type": "array", "items": { @@ -7354,6 +7374,10 @@ "type": "boolean", "example": false }, + "keep_firing_for": { + "type": "string", + "format": "duration" + }, "labels": { "type": "object", "additionalProperties": { diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 58501b2cda8..c45de3ca093 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -34,11 +34,10 @@ var ( // ErrAlertRuleFailedGenerateUniqueUID is an error for failure to generate alert rule UID ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID") // ErrCannotEditNamespace is an error returned if the user does not have permissions to edit the namespace - ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace") - ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace") - ErrAlertRuleFailedValidation = errors.New("invalid alert rule") - ErrAlertRuleUniqueConstraintViolation = errors.New("rule title under the same organisation and folder should be unique") - ErrQuotaReached = errors.New("quota has been exceeded") + ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace") + ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace") + ErrAlertRuleFailedValidation = errors.New("invalid alert rule") + ErrQuotaReached = errors.New("quota has been exceeded") // ErrNoDashboard is returned when the alert rule does not have a Dashboard UID // in its annotations or the dashboard does not exist. ErrNoDashboard = errors.New("no dashboard") diff --git a/pkg/services/ngalert/models/errors.go b/pkg/services/ngalert/models/errors.go index 338cc7709f2..ea2a1b047ab 100644 --- a/pkg/services/ngalert/models/errors.go +++ b/pkg/services/ngalert/models/errors.go @@ -28,11 +28,13 @@ func ErrAlertRuleConflictVerbose(existingPartialRule, rule AlertRule, underlying "RuleUID": rule.UID, "Title": rule.Title, "NamespaceUID": rule.NamespaceUID, + "RuleGroup": rule.RuleGroup, }, "Existing": map[string]any{ "RuleUID": existingPartialRule.UID, "Title": existingPartialRule.Title, "NamespaceUID": existingPartialRule.NamespaceUID, + "RuleGroup": existingPartialRule.RuleGroup, }, "Error": underlying.Error(), }, Error: underlying}) diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index 483ec67929e..b433a2ec76b 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -134,7 +134,6 @@ func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, g } func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup PrometheusRuleGroup) (*models.AlertRuleGroup, error) { - uniqueNames := map[string]int{} rules := make([]models.AlertRule, 0, len(promGroup.Rules)) interval := time.Duration(promGroup.Interval) @@ -150,12 +149,6 @@ func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup gr.RuleGroupIndex = i + 1 gr.IntervalSeconds = int64(interval.Seconds()) - // Check rule title uniqueness within the group. - uniqueNames[gr.Title]++ - if val := uniqueNames[gr.Title]; val > 1 { - gr.Title = fmt.Sprintf("%s (%d)", gr.Title, val) - } - uid, err := getUID(orgID, namespaceUID, promGroup.Name, i, rule) if err != nil { return nil, fmt.Errorf("failed to generate UID for rule '%s': %w", gr.Title, err) @@ -206,7 +199,7 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom var err error isRecordingRule := rule.Record != "" - query, err = p.createQuery(rule.Expr, isRecordingRule) + query, err = p.createQuery(rule.Expr, isRecordingRule, promGroup) if err != nil { return models.AlertRule{}, err } @@ -225,13 +218,6 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom title = rule.Alert } - // Temporary workaround for avoiding the uniqueness check for the rule title. - // In Grafana alert rule titles must be unique within the same org and folder, - // but Prometheus allows multiple rules with the same name. By adding the group name - // to the title we ensure that the title is unique within the group. - // TODO: Remove this workaround when we have a proper solution for handling rule title uniqueness. - title = fmt.Sprintf("[%s] %s", promGroup.Name, title) - labels := make(map[string]string, len(rule.Labels)+len(promGroup.Labels)) maps.Copy(labels, promGroup.Labels) maps.Copy(labels, rule.Labels) @@ -255,6 +241,12 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom RuleGroup: promGroup.Name, IsPaused: isPaused, Record: record, + + // MissingSeriesEvalsToResolve is set to 1 to match the Prometheus behaviour. + // Prometheus resolves alerts as soon as the series disappears. + // By setting this value to 1 we ensure that the alert is resolved on the first evaluation + // that doesn't have the series. + MissingSeriesEvalsToResolve: util.Pointer(1), } if p.cfg.KeepOriginalRuleDefinition != nil && *p.cfg.KeepOriginalRuleDefinition { @@ -279,8 +271,16 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom // // This is needed to ensure that we keep the Prometheus behaviour, where any returned result // is considered alerting, and only when the query returns no data is the alert treated as normal. -func (p *Converter) createQuery(expr string, isRecordingRule bool) ([]models.AlertQuery, error) { - queryNode, err := createQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, expr, *p.cfg.FromTimeRange, *p.cfg.EvaluationOffset) +func (p *Converter) createQuery(expr string, isRecordingRule bool, promGroup PrometheusRuleGroup) ([]models.AlertQuery, error) { + // If evaluation offset is set on the group level, use that, otherwise use the global evaluation offset. + var evaluationOffset time.Duration + if promGroup.QueryOffset != nil { + evaluationOffset = time.Duration(*promGroup.QueryOffset) + } else { + evaluationOffset = *p.cfg.EvaluationOffset + } + + queryNode, err := createQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, expr, *p.cfg.FromTimeRange, evaluationOffset) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 5f039e69831..479417bc6b8 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -36,8 +36,9 @@ func TestPrometheusRulesToGrafana(t *testing.T) { orgID: 1, namespace: "some-namespace-uid", promGroup: PrometheusRuleGroup{ - Name: "test-group-1", - Interval: prommodel.Duration(10 * time.Second), + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + QueryOffset: util.Pointer(prommodel.Duration(1 * time.Minute)), Rules: []PrometheusRule{ { Alert: "alert-1", @@ -124,16 +125,13 @@ func TestPrometheusRulesToGrafana(t *testing.T) { expectError: false, }, { - name: "rule group with query_offset is not supported", + name: "query_offset must be >= 0", orgID: 1, namespace: "namespaceUID", promGroup: PrometheusRuleGroup{ - Name: "test-group-1", - Interval: prommodel.Duration(10 * time.Second), - QueryOffset: func() *prommodel.Duration { - d := prommodel.Duration(30 * time.Second) - return &d - }(), + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + QueryOffset: util.Pointer(prommodel.Duration(-1)), Rules: []PrometheusRule{ { Alert: "alert-1", @@ -142,7 +140,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, }, expectError: true, - errorMsg: "query_offset is not supported", + errorMsg: "query_offset must be >= 0", }, { name: "rule group with limit is not supported", @@ -179,6 +177,32 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, expectError: false, }, + { + name: "when global query offset is set, it should be used", + orgID: 1, + namespace: "some-namespace-uid", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "cpu_usage > 80", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "CPU usage is critical", + }, + }, + }, + }, + config: Config{ + EvaluationOffset: util.Pointer(5 * time.Minute), + }, + expectError: false, + }, } for _, tc := range testCases { @@ -214,7 +238,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { grafanaRule := grafanaGroup.Rules[j] if promRule.Record != "" { - require.Equal(t, fmt.Sprintf("[%s] %s", tc.promGroup.Name, promRule.Record), grafanaRule.Title) + require.Equal(t, promRule.Record, grafanaRule.Title) require.NotNil(t, grafanaRule.Record) require.Equal(t, grafanaRule.Record.From, queryRefID) require.Equal(t, promRule.Record, grafanaRule.Record.Metric) @@ -225,7 +249,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { } require.Equal(t, targetDatasourceUID, grafanaRule.Record.TargetDatasourceUID) } else { - require.Equal(t, fmt.Sprintf("[%s] %s", tc.promGroup.Name, promRule.Alert), grafanaRule.Title) + require.Equal(t, promRule.Alert, grafanaRule.Title) } var expectedFor time.Duration @@ -244,8 +268,19 @@ func TestPrometheusRulesToGrafana(t *testing.T) { require.Equal(t, expectedLabels, grafanaRule.Labels, tc.name) require.Equal(t, promRule.Annotations, grafanaRule.Annotations, tc.name) - require.Equal(t, models.Duration(0*time.Minute), grafanaRule.Data[0].RelativeTimeRange.To) - require.Equal(t, models.Duration(10*time.Minute), grafanaRule.Data[0].RelativeTimeRange.From) + + evalOffset := time.Duration(0) + if tc.config.EvaluationOffset != nil { + evalOffset = *tc.config.EvaluationOffset + } + if tc.promGroup.QueryOffset != nil { + // group-level offset takes precedence + evalOffset = time.Duration(*tc.promGroup.QueryOffset) + } + + require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To) + require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From) + require.Equal(t, util.Pointer(1), grafanaRule.MissingSeriesEvalsToResolve) originalRuleDefinition, err := yaml.Marshal(promRule) require.NoError(t, err) @@ -292,10 +327,10 @@ func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) { require.Equal(t, "test-group-1", group.Title) require.Len(t, group.Rules, 4) - require.Equal(t, "[test-group-1] alert", group.Rules[0].Title) - require.Equal(t, "[test-group-1] alert (2)", group.Rules[1].Title) - require.Equal(t, "[test-group-1] another alert", group.Rules[2].Title) - require.Equal(t, "[test-group-1] alert (3)", group.Rules[3].Title) + require.Equal(t, "alert", group.Rules[0].Title) + require.Equal(t, "alert", group.Rules[1].Title) + require.Equal(t, "another alert", group.Rules[2].Title) + require.Equal(t, "alert", group.Rules[3].Title) } func TestCreateMathNode(t *testing.T) { diff --git a/pkg/services/ngalert/prom/models.go b/pkg/services/ngalert/prom/models.go index 21ea45061dc..0ef0a301885 100644 --- a/pkg/services/ngalert/prom/models.go +++ b/pkg/services/ngalert/prom/models.go @@ -25,14 +25,14 @@ type PrometheusRuleGroup struct { } func (g *PrometheusRuleGroup) Validate() error { - if g.QueryOffset != nil { - return ErrPrometheusRuleGroupValidationFailed.Errorf("query_offset is not supported") - } - if g.Limit != 0 { return ErrPrometheusRuleGroupValidationFailed.Errorf("limit is not supported") } + if g.QueryOffset != nil && *g.QueryOffset < prommodel.Duration(0) { + return ErrPrometheusRuleGroupValidationFailed.Errorf("query_offset must be >= 0") + } + for _, rule := range g.Rules { if err := rule.Validate(); err != nil { return err diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go index b2bdb734b44..90be51c140c 100644 --- a/pkg/services/ngalert/prom/models_test.go +++ b/pkg/services/ngalert/prom/models_test.go @@ -26,6 +26,7 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { Labels: map[string]string{ "label-1": "value-1", }, + QueryOffset: util.Pointer(prommodel.Duration(time.Duration(1) * time.Second)), Rules: []PrometheusRule{ { Alert: "test_alert", @@ -36,14 +37,14 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { expectError: false, }, { - name: "invalid group with query_offset", + name: "invalid group with negative query_offset", group: PrometheusRuleGroup{ Name: "test_group", Interval: prommodel.Duration(60), - QueryOffset: util.Pointer(prommodel.Duration(10)), + QueryOffset: util.Pointer(prommodel.Duration(-1)), }, expectError: true, - errorMsg: "query_offset is not supported", + errorMsg: "query_offset must be >= 0", }, { name: "invalid group with limit", diff --git a/pkg/services/ngalert/prom/query.go b/pkg/services/ngalert/prom/query.go index 74aed34f83f..8e28b0ac3e7 100644 --- a/pkg/services/ngalert/prom/query.go +++ b/pkg/services/ngalert/prom/query.go @@ -43,7 +43,7 @@ func createQueryNode(datasourceUID, datasourceType, expr string, fromTimeRange, RefID: queryRefID, RelativeTimeRange: models.RelativeTimeRange{ From: models.Duration(fromTimeRange + evaluationOffset), - To: models.Duration(0 + evaluationOffset), + To: models.Duration(evaluationOffset), }, }, nil } diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 5928c8f6dcb..3f0821889d2 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -255,17 +255,22 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { return err } - decrypted, err := am.decryptConfiguration(ctx, c) + rawDecrypted, configHash, err := am.decryptConfiguration(ctx, c) if err != nil { return err } // Send the configuration only if we need to. - if !am.shouldSendConfig(ctx, decrypted) { + if !am.shouldSendConfig(ctx, configHash) { return nil } - isDefault, err := am.isDefaultConfiguration(decrypted) + isDefault, err := am.isDefaultConfiguration(configHash) + if err != nil { + return err + } + + decrypted, err := notifier.Load(rawDecrypted) if err != nil { return err } @@ -273,27 +278,37 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config return am.sendConfiguration(ctx, decrypted, config.ConfigurationHash, config.CreatedAt, isDefault) } -func (am *Alertmanager) isDefaultConfiguration(cfg *apimodels.PostableUserConfig) (bool, error) { - rawCfg, err := json.Marshal(cfg) - if err != nil { - return false, err - } - - configHash := fmt.Sprintf("%x", md5.Sum(rawCfg)) - - return configHash == am.defaultConfigHash, nil +func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) (bool, error) { + return fmt.Sprintf("%x", configHash) == am.defaultConfigHash, nil } -func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) (*apimodels.PostableUserConfig, error) { +// decryptConfiguration decrypts the configuration in-place and returns the decrypted configuration alongside its hash. +// Should not be used outside of this package and the specific use case of decrypting the configuration before sending +// it to the remote Alertmanager. +func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) ([]byte, [16]byte, error) { fn := func(payload []byte) ([]byte, error) { return am.decrypt(ctx, payload) } - decrypted, err := cfg.Decrypt(fn) - if err != nil { - return nil, fmt.Errorf("unable to decrypt the configuration: %w", err) + + // Iterate through receivers and decrypt secure settings. + // It's not necessary to be careful about not modifying the original, as it's used only in a specific context where + // the config is read from json and then immediately sent to the remote Alertmanager. + for _, rcv := range cfg.AlertmanagerConfig.Receivers { + for _, gmr := range rcv.PostableGrafanaReceivers.GrafanaManagedReceivers { + decrypted, err := gmr.DecryptSecureSettings(fn) + if err != nil { + return nil, [16]byte{}, fmt.Errorf("unable to decrypt settings on receiver %q (uid: %q): %w", gmr.Name, gmr.UID, err) + } + gmr.SecureSettings = decrypted + } } - return &decrypted, nil + rawDecrypted, err := json.Marshal(cfg) + if err != nil { + return nil, [16]byte{}, fmt.Errorf("unable to marshal decrypted configuration: %w", err) + } + + return rawDecrypted, md5.Sum(rawDecrypted), nil } func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { @@ -345,7 +360,12 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P if err := am.autogenFn(ctx, am.log, am.orgID, &cfg.AlertmanagerConfig, false); err != nil { return err } - decrypted, err := am.decryptConfiguration(ctx, cfg) + rawDecrypted, _, err := am.decryptConfiguration(ctx, cfg) + if err != nil { + return err + } + + decrypted, err := notifier.Load(rawDecrypted) if err != nil { return err } @@ -364,7 +384,12 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { return err } - decrypted, err := am.decryptConfiguration(ctx, c) + rawDecrypted, _, err := am.decryptConfiguration(ctx, c) + if err != nil { + return err + } + + decrypted, err := notifier.Load(rawDecrypted) if err != nil { return err } @@ -634,7 +659,7 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) { // shouldSendConfig compares the remote Alertmanager configuration with our local one. // It returns true if the configurations are different. -func (am *Alertmanager) shouldSendConfig(ctx context.Context, config *apimodels.PostableUserConfig) bool { +func (am *Alertmanager) shouldSendConfig(ctx context.Context, hash [16]byte) bool { rc, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) if err != nil { // Log the error and return true so we try to upload our config anyway. @@ -651,12 +676,7 @@ func (am *Alertmanager) shouldSendConfig(ctx context.Context, config *apimodels. am.log.Error("Unable to marshal the remote Alertmanager configuration for comparison", "err", err) return true } - rawInternal, err := json.Marshal(config) - if err != nil { - am.log.Error("Unable to marshal the internal Alertmanager configuration for comparison", "err", err) - return true - } - return md5.Sum(rawRemote) != md5.Sum(rawInternal) + return md5.Sum(rawRemote) != hash } // shouldSendState compares the remote Alertmanager state with our local one. diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 7ad086dc7f2..3d9f10e4396 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -23,6 +23,8 @@ import ( "github.com/grafana/alerting/definition" alertingModels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" + "gopkg.in/yaml.v3" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -39,13 +41,12 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" - "gopkg.in/yaml.v3" ) const ( // Valid Grafana Alertmanager configurations. - testGrafanaConfig = `{"template_files":{},"alertmanager_config":{"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"}}]}]}}` - testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"test"}}]}]}}` + testGrafanaConfig = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"}}]}]}}` + testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"test"}}]}]}}` testGrafanaDefaultConfigWithDifferentFieldOrder = `{"alertmanager_config":{"route":{"group_by":["alertname","grafana_folder"],"receiver":"grafana-default-email"},"receivers":[{"grafana_managed_receiver_configs":[{"uid":"","name":"email receiver","type":"email","settings":{"addresses":""}}],"name":"grafana-default-email"}]}}` // Valid Alertmanager state base64 encoded. @@ -287,14 +288,14 @@ func TestCompareAndSendConfiguration(t *testing.T) { strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, `"password":"!"`, 1), NoopAutogenFn, nil, - "unable to decrypt the configuration: failed to decode value for key 'password': illegal base64 data at input byte 0", + `unable to decrypt settings on receiver "WH" (uid: "dde6ntuob69dtf"): failed to decode value for key 'password': illegal base64 data at input byte 0`, }, { "decrypt error", testGrafanaConfigWithSecret, NoopAutogenFn, nil, - fmt.Sprintf("unable to decrypt the configuration: failed to decrypt value for key 'password': %s", errTest.Error()), + fmt.Sprintf(`unable to decrypt settings on receiver "WH" (uid: "dde6ntuob69dtf"): failed to decrypt value for key 'password': %s`, errTest.Error()), }, { "error from autogen function", @@ -443,7 +444,9 @@ func Test_isDefaultConfiguration(t *testing.T) { defaultConfig: string(rawDefaultCfg), defaultConfigHash: fmt.Sprintf("%x", md5.Sum(rawDefaultCfg)), } - isDefault, _ := am.isDefaultConfiguration(test.config) + raw, err := json.Marshal(test.config) + require.NoError(tt, err) + isDefault, _ := am.isDefaultConfiguration(md5.Sum(raw)) require.Equal(tt, test.expected, isDefault) }) } diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 4482ee0495a..0ae532b01ad 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -13,6 +13,7 @@ import ( "strings" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -139,7 +140,12 @@ func (mc *Mimir) do(ctx context.Context, p, method string, payload io.Reader, ou ct := resp.Header.Get("Content-Type") if !strings.HasPrefix(ct, "application/json") { msg := "Response content-type is not application/json" - mc.logger.Error(msg, "content-type", "url", r.URL.String(), "method", r.Method, ct, "status", resp.StatusCode) + body, err := io.ReadAll(resp.Body) + bodyStr := string(body) + if err != nil { + bodyStr = fmt.Sprintf("fail_to_read: %s", err) + } + mc.logger.Error(msg, "content-type", "url", r.URL.String(), "method", r.Method, ct, "status", resp.StatusCode, "body", bodyStr) return nil, fmt.Errorf("%s: %s", msg, ct) } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 4df2410ed05..c747e4461e5 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -374,7 +373,7 @@ func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, for i := range newRules { if _, err := sess.Insert(&newRules[i]); err != nil { if st.SQLStore.GetDialect().IsUniqueConstraintViolation(err) { - return ruleConstraintViolationToErr(sess, rules[i], err, st.Logger) + return ruleConstraintViolationToErr(rules[i], err) } return fmt.Errorf("failed to create new rules: %w", err) } @@ -432,7 +431,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, if updated, err := sess.ID(r.Existing.ID).AllCols().Omit("rule_guid").Update(converted); err != nil || updated == 0 { if err != nil { if st.SQLStore.GetDialect().IsUniqueConstraintViolation(err) { - return ruleConstraintViolationToErr(sess, r.New, err, st.Logger) + return ruleConstraintViolationToErr(r.New, err) } return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err) } @@ -1201,22 +1200,9 @@ func (st DBstore) RenameTimeIntervalInNotificationSettings( return result, nil, st.UpdateAlertRules(ctx, &ngmodels.AlertingUserUID, updates) } -func ruleConstraintViolationToErr(sess *db.Session, rule ngmodels.AlertRule, err error, logger log.Logger) error { +func ruleConstraintViolationToErr(rule ngmodels.AlertRule, err error) error { msg := err.Error() - if strings.Contains(msg, "UQE_alert_rule_org_id_namespace_uid_title") || strings.Contains(msg, "alert_rule.org_id, alert_rule.namespace_uid, alert_rule.title") { - // return verbose conflicting alert rule error response - // see: https://github.com/grafana/grafana/issues/89755 - var fetched_uid string - var existingPartialAlertRule ngmodels.AlertRule - ok, uid_fetch_err := sess.Table("alert_rule").Cols("uid").Where("org_id = ? AND title = ? AND namespace_uid = ?", rule.OrgID, rule.Title, rule.NamespaceUID).Get(&fetched_uid) - if uid_fetch_err != nil { - logger.Error("Error fetching uid from alert_rule table", "reason", uid_fetch_err.Error()) - } - if ok { - existingPartialAlertRule = ngmodels.AlertRule{UID: fetched_uid, Title: rule.Title, NamespaceUID: rule.NamespaceUID} - } - return ngmodels.ErrAlertRuleConflictVerbose(existingPartialAlertRule, rule, ngmodels.ErrAlertRuleUniqueConstraintViolation) - } else if strings.Contains(msg, "UQE_alert_rule_org_id_uid") || strings.Contains(msg, "alert_rule.org_id, alert_rule.uid") { + if strings.Contains(msg, "UQE_alert_rule_org_id_uid") || strings.Contains(msg, "alert_rule.org_id, alert_rule.uid") { // return verbose conflicting alert rule error response // see: https://github.com/grafana/grafana/issues/89755 existingPartialAlertRule := ngmodels.AlertRule{UID: rule.UID} diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 1567740b7dd..11614bb6841 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/benbjohnson/clock" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -230,257 +231,6 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { }) } -func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - usr := models.UserUID("test") - cfg := setting.NewCfg() - cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second} - sqlStore := db.InitTestDB(t) - folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) - b := &fakeBus{} - store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) - - gen := models.RuleGen - createRuleInFolder := func(title string, orgID int64, namespaceUID string) *models.AlertRule { - gen := gen.With( - gen.WithOrgID(orgID), - gen.WithIntervalMatching(store.Cfg.BaseInterval), - gen.WithNamespaceUID(namespaceUID), - ) - return createRule(t, store, gen) - } - - t.Run("should handle update chains without unique constraint violation", func(t *testing.T) { - rule1 := createRuleInFolder("chain-rule1", 1, "my-namespace") - rule2 := createRuleInFolder("chain-rule2", 1, "my-namespace") - - newRule1 := models.CopyRule(rule1) - newRule2 := models.CopyRule(rule2) - newRule1.Title = rule2.Title - newRule2.Title = util.GenerateShortUID() - - err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ - Existing: rule1, - New: *newRule1, - }, { - Existing: rule2, - New: *newRule2, - }, - }) - require.NoError(t, err) - - dbrule1 := &alertRule{} - dbrule2 := &alertRule{} - err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { - exist, err := sess.Table(alertRule{}).ID(rule1.ID).Get(dbrule1) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule1.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule2.ID).Get(dbrule2) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule2.ID)) - return nil - }) - - require.NoError(t, err) - require.Equal(t, newRule1.Title, dbrule1.Title) - require.Equal(t, newRule2.Title, dbrule2.Title) - }) - - t.Run("should handle update chains with cycle without unique constraint violation", func(t *testing.T) { - rule1 := createRuleInFolder("cycle-rule1", 1, "my-namespace") - rule2 := createRuleInFolder("cycle-rule2", 1, "my-namespace") - rule3 := createRuleInFolder("cycle-rule3", 1, "my-namespace") - - newRule1 := models.CopyRule(rule1) - newRule2 := models.CopyRule(rule2) - newRule3 := models.CopyRule(rule3) - newRule1.Title = rule2.Title - newRule2.Title = rule3.Title - newRule3.Title = rule1.Title - - err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ - Existing: rule1, - New: *newRule1, - }, { - Existing: rule2, - New: *newRule2, - }, { - Existing: rule3, - New: *newRule3, - }, - }) - require.NoError(t, err) - - dbrule1 := &alertRule{} - dbrule2 := &alertRule{} - dbrule3 := &alertRule{} - err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { - exist, err := sess.Table(alertRule{}).ID(rule1.ID).Get(dbrule1) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule1.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule2.ID).Get(dbrule2) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule2.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule3.ID).Get(dbrule3) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule3.ID)) - return nil - }) - - require.NoError(t, err) - require.Equal(t, newRule1.Title, dbrule1.Title) - require.Equal(t, newRule2.Title, dbrule2.Title) - require.Equal(t, newRule3.Title, dbrule3.Title) - }) - - t.Run("should handle case-insensitive intermediate collision without unique constraint violation", func(t *testing.T) { - rule1 := createRuleInFolder("case-cycle-rule1", 1, "my-namespace") - rule2 := createRuleInFolder("case-cycle-rule2", 1, "my-namespace") - - newRule1 := models.CopyRule(rule1) - newRule2 := models.CopyRule(rule2) - newRule1.Title = strings.ToUpper(rule2.Title) - newRule2.Title = strings.ToUpper(rule1.Title) - - err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ - Existing: rule1, - New: *newRule1, - }, { - Existing: rule2, - New: *newRule2, - }, - }) - require.NoError(t, err) - - dbrule1 := &alertRule{} - dbrule2 := &alertRule{} - err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { - exist, err := sess.Table(alertRule{}).ID(rule1.ID).Get(dbrule1) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule1.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule2.ID).Get(dbrule2) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule2.ID)) - return nil - }) - - require.NoError(t, err) - require.Equal(t, newRule1.Title, dbrule1.Title) - require.Equal(t, newRule2.Title, dbrule2.Title) - }) - - t.Run("should handle update multiple chains in different folders without unique constraint violation", func(t *testing.T) { - rule1 := createRuleInFolder("multi-cycle-rule1", 1, "my-namespace") - rule2 := createRuleInFolder("multi-cycle-rule2", 1, "my-namespace") - rule3 := createRuleInFolder("multi-cycle-rule1", 1, "my-namespace2") - rule4 := createRuleInFolder("multi-cycle-rule2", 1, "my-namespace2") - - newRule1 := models.CopyRule(rule1) - newRule2 := models.CopyRule(rule2) - newRule3 := models.CopyRule(rule3) - newRule4 := models.CopyRule(rule4) - newRule1.Title = rule2.Title - newRule2.Title = rule1.Title - newRule3.Title = rule4.Title - newRule4.Title = rule3.Title - - err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ - Existing: rule1, - New: *newRule1, - }, { - Existing: rule2, - New: *newRule2, - }, { - Existing: rule3, - New: *newRule3, - }, { - Existing: rule4, - New: *newRule4, - }, - }) - require.NoError(t, err) - - dbrule1 := &alertRule{} - dbrule2 := &alertRule{} - dbrule3 := &alertRule{} - dbrule4 := &alertRule{} - err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { - exist, err := sess.Table(alertRule{}).ID(rule1.ID).Get(dbrule1) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule1.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule2.ID).Get(dbrule2) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule2.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule3.ID).Get(dbrule3) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule3.ID)) - - exist, err = sess.Table(alertRule{}).ID(rule4.ID).Get(dbrule4) - if err != nil { - return err - } - require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule4.ID)) - return nil - }) - - require.NoError(t, err) - require.Equal(t, newRule1.Title, dbrule1.Title) - require.Equal(t, newRule2.Title, dbrule2.Title) - require.Equal(t, newRule3.Title, dbrule3.Title) - require.Equal(t, newRule4.Title, dbrule4.Title) - }) - - t.Run("should fail with unique constraint violation", func(t *testing.T) { - rule1 := createRuleInFolder("unique-rule1", 1, "my-namespace") - rule2 := createRuleInFolder("unique-rule2", 1, "my-namespace") - - newRule1 := models.CopyRule(rule1) - newRule2 := models.CopyRule(rule2) - newRule2.Title = newRule1.Title - - err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ - Existing: rule2, - New: *newRule2, - }, - }) - require.ErrorIs(t, err, models.ErrAlertRuleUniqueConstraintViolation) - require.NotEqual(t, newRule2.UID, "") - require.NotEqual(t, newRule2.Title, "") - require.NotEqual(t, newRule2.NamespaceUID, "") - require.ErrorContains(t, err, newRule2.UID) - require.ErrorContains(t, err, newRule2.Title) - require.ErrorContains(t, err, newRule2.NamespaceUID) - }) -} - func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -1078,18 +828,11 @@ func TestIntegrationInsertAlertRules(t *testing.T) { _, err = store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{rules[0]}) require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) }) - t.Run("fail insert rules with the same title in a folder", func(t *testing.T) { + t.Run("should not fail insert rules with the same title in a folder", func(t *testing.T) { cp := models.CopyRule(&rules[0]) cp.UID = cp.UID + "-new" _, err = store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{*cp}) - require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) - require.ErrorIs(t, err, models.ErrAlertRuleUniqueConstraintViolation) - require.NotEqual(t, rules[0].UID, "") - require.NotEqual(t, rules[0].Title, "") - require.NotEqual(t, rules[0].NamespaceUID, "") - require.ErrorContains(t, err, rules[0].UID) - require.ErrorContains(t, err, rules[0].Title) - require.ErrorContains(t, err, rules[0].NamespaceUID) + require.NoError(t, err) }) t.Run("should not let insert rules with the same UID", func(t *testing.T) { cp := models.CopyRule(&rules[0]) @@ -2078,6 +1821,15 @@ func TestIntegration_ListDeletedRules(t *testing.T) { store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore) + oldT := TimeNow + t.Cleanup(func() { + TimeNow = oldT + }) + clk := clock.NewMock() + TimeNow = func() time.Time { + return clk.Now() + } + orgID := int64(1) gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID)) @@ -2087,6 +1839,7 @@ func TestIntegration_ListDeletedRules(t *testing.T) { rule, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: result[0].UID}) require.NoError(t, err) + clk.Add(1 * time.Hour) rule2 := models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())) err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{ { @@ -2108,7 +1861,8 @@ func TestIntegration_ListDeletedRules(t *testing.T) { require.Empty(t, list) }) - err = store.DeleteAlertRulesByUID(context.Background(), orgID, &models.AlertingUserUID, false, rule.UID) + clk.Add(1 * time.Hour) + err = store.DeleteAlertRulesByUID(context.Background(), orgID, util.Pointer(models.UserUID("test")), false, rule.UID) require.NoError(t, err) t.Run("should return the last deleted rule", func(t *testing.T) { @@ -2116,7 +1870,9 @@ func TestIntegration_ListDeletedRules(t *testing.T) { require.NoError(t, err) require.Len(t, list, 1) assert.Empty(t, list[0].UID) - assert.Empty(t, rule2.Diff(list[0], "ID", "UID", "DashboardUID", "PanelID")) + assert.Empty(t, rule2.Diff(list[0], "ID", "UID", "DashboardUID", "PanelID", "Updated", "UpdatedBy")) // ignore updated because it's not + assert.Equal(t, list[0].Updated.UTC(), clk.Now().UTC()) + assert.EqualValues(t, list[0].UpdatedBy, util.Pointer(models.UserUID("test"))) }) } diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 0e13a46834d..daa61da93d3 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -8,6 +8,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -67,6 +69,8 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(tb, err) dashboardService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/ngalert/writer/prom.go b/pkg/services/ngalert/writer/prom.go index 5ec05f56571..40648e65628 100644 --- a/pkg/services/ngalert/writer/prom.go +++ b/pkg/services/ngalert/writer/prom.go @@ -24,12 +24,80 @@ import ( const backendType = "prometheus" const ( - // Fixed error messages - MimirDuplicateTimestampError = "err-mimir-sample-duplicate-timestamp" - MimirInvalidLabelError = "err-mimir-label-invalid" - MimirLabelValueTooLongError = "err-mimir-label-value-too-long" - MimirMaxLabelNamesPerSeriesError = "err-mimir-max-label-names-per-series" - MimirMaxSeriesPerUserError = "err-mimir-max-series-per-user" + // NOTE: Mimir errors were copied from globalerror package: + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/util/globalerror/user.go + // Variable names have been standardized as Mimir+{globalerror.ID}+Error for consistency + // We could consider importing those directly from mimir or moving them to a shared package + // Other than that, error codes are mapped in errorCauseToHTTPStatusCode (distributor package): + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/errors.go#L301-L301 + // The following causes are mapped to Bad Request (400): + // - mimirpb.TOO_MANY_CLUSTERS: + // - mimirpb.BAD_DATA: + // - mimirpb.TENANT_LIMIT: + + // Handler checks for write message size limits + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/push.go#L92-L92 + MimirDistributorMaxWriteMessageSizeError = "err-mimir-distributor-max-write-message-size" + MimirDistributorMaxWriteRequestDataItemSizeError = "err-mimir-distributor-max-write-request-data-item-size" + + // Distributor.prePushValidationMiddleware calls: 1. validateLabels, 2. validateSamples, 3. validateHistograms, + // 4. validateExamplars, 5. cleanAndValidateMetadata, then 6. checks for ingestion rate limits + // 1. validateLabel errors + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/validate.go#L402-L402 + MimirInvalidMetricNameError = "err-mimir-metric-name-invalid" + MimirMaxLabelNamesPerInfoSeriesError = "err-mimir-max-label-names-per-info-series" + MimirMaxLabelNamesPerSeriesError = "err-mimir-max-label-names-per-series" + MimirMissingMetricNameError = "err-mimir-missing-metric-name" + MimirSeriesInvalidLabelError = "err-mimir-label-invalid" + MimirSeriesInvalidLabelValueError = "err-mimir-label-value-invalid" + MimirSeriesLabelNameTooLongError = "err-mimir-label-name-too-long" + MimirSeriesLabelValueTooLongError = "err-mimir-label-value-too-long" + MimirSeriesWithDuplicateLabelNamesError = "err-mimir-duplicate-label-names" + + // 2. validateSamples errors + MimirSampleTooFarInFutureError = "err-mimir-too-far-in-future" + MimirSampleTooFarInPastError = "err-mimir-too-far-in-past" + + // 3. validateHistograms + MimirInvalidSchemaNativeHistogramError = "err-mimir-invalid-native-histogram-schema" + MimirMaxNativeHistogramBucketsError = "err-mimir-max-native-histogram-buckets" + MimirNotReducibleNativeHistogramError = "err-mimir-not-reducible-native-histogram" + + // 4. validateExemplars + MimirExemplarLabelsMissingError = "err-mimir-exemplar-labels-missing" + MimirExemplarLabelsTooLongError = "err-mimir-exemplar-labels-too-long" + MimirExemplarTimestampInvalidError = "err-mimir-exemplar-timestamp-invalid" + + // 5. cleanAndValidateMetadata errors + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/validate.go#L491-L491 + MimirMetricMetadataMetricNameTooLongError = "err-mimir-metric-name-too-long" + MimirMetricMetadataMissingMetricNameError = "err-mimir-metadata-missing-metric-name" + MimirMetricMetadataUnitTooLongError = "err-mimir-unit-too-long" + + // 6. ingestion rate limited error + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/distributor.go#L1317-L1317 + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/distributor/distributor.go#L1324-L1324 + MimirIngestionRateLimitedError = "err-mimir-tenant-max-ingestion-rate" + + // Ingester.PushWithCleanup errors + // https://github.com/grafana/mimir/blob/1ff367ef58987cd1941de03a8d6923fde82dfdd3/pkg/ingester/ingester.go#L1254-L1254 + MimirExemplarSeriesMissingError = "err-mimir-exemplar-series-missing" + MimirExemplarTooFarInFutureError = "err-mimir-exemplar-too-far-in-future" + MimirExemplarTooFarInPastError = "err-mimir-exemplar-too-far-in-past" + MimirMaxMetadataPerMetricError = "err-mimir-max-metadata-per-metric" + MimirMaxMetadataPerUserError = "err-mimir-max-metadata-per-user" + MimirMaxSeriesPerMetricError = "err-mimir-max-series-per-metric" + MimirMaxSeriesPerUserError = "err-mimir-max-series-per-user" + MimirNativeHistogramCountMismatchError = "err-mimir-native-histogram-count-mismatch" + MimirNativeHistogramCountNotBigEnoughError = "err-mimir-native-histogram-count-not-big-enough" + MimirNativeHistogramNegativeBucketCountError = "err-mimir-native-histogram-negative-bucket-count" + MimirNativeHistogramOOODisabledError = "err-mimir-native-histogram-ooo-disabled" + MimirNativeHistogramSpanNegativeOffsetError = "err-mimir-native-histogram-span-negative-offset" + MimirNativeHistogramSpansBucketsMismatchError = "err-mimir-native-histogram-spans-buckets-mismatch" + MimirSampleDuplicateTimestampError = "err-mimir-sample-duplicate-timestamp" + MimirSampleOutOfOrderError = "err-mimir-sample-out-of-order" + MimirSampleTimestampTooOldError = "err-mimir-sample-timestamp-too-old" + MimirTooManyHAClustersError = "err-mimir-tenant-too-many-ha-clusters" // Best effort error messages PrometheusDuplicateTimestampError = "duplicate sample for timestamp" @@ -41,12 +109,56 @@ var ( // Expected, user-level write errors like trying to write an invalid series. ErrRejectedWrite = errors.New("series was rejected") ErrBadFrame = errors.New("failed to read dataframe") -) -var DuplicateTimestampErrors = [...]string{ - MimirDuplicateTimestampError, - PrometheusDuplicateTimestampError, -} + // IgnoredErrors don't cause the Write to fail, but are still logged. + IgnoredErrors = []string{ + MimirSampleDuplicateTimestampError, + PrometheusDuplicateTimestampError, + } + + // ExpectedErrors are user-level write errors like trying to write an invalid series. + ExpectedErrors = []string{ + MimirDistributorMaxWriteMessageSizeError, + MimirDistributorMaxWriteRequestDataItemSizeError, + MimirExemplarLabelsMissingError, + MimirExemplarLabelsTooLongError, + MimirExemplarSeriesMissingError, + MimirExemplarTimestampInvalidError, + MimirExemplarTooFarInFutureError, + MimirExemplarTooFarInPastError, + MimirIngestionRateLimitedError, + MimirInvalidMetricNameError, + MimirInvalidSchemaNativeHistogramError, + MimirMaxLabelNamesPerInfoSeriesError, + MimirMaxLabelNamesPerSeriesError, + MimirMaxMetadataPerMetricError, + MimirMaxMetadataPerUserError, + MimirMaxNativeHistogramBucketsError, + MimirMaxSeriesPerMetricError, + MimirMaxSeriesPerUserError, + MimirMetricMetadataMetricNameTooLongError, + MimirMetricMetadataMissingMetricNameError, + MimirMetricMetadataUnitTooLongError, + MimirMissingMetricNameError, + MimirNativeHistogramCountMismatchError, + MimirNativeHistogramCountNotBigEnoughError, + MimirNativeHistogramNegativeBucketCountError, + MimirNativeHistogramOOODisabledError, + MimirNativeHistogramSpanNegativeOffsetError, + MimirNativeHistogramSpansBucketsMismatchError, + MimirNotReducibleNativeHistogramError, + MimirSampleOutOfOrderError, + MimirSampleTimestampTooOldError, + MimirSampleTooFarInFutureError, + MimirSampleTooFarInPastError, + MimirSeriesInvalidLabelError, + MimirSeriesInvalidLabelValueError, + MimirSeriesLabelNameTooLongError, + MimirSeriesLabelValueTooLongError, + MimirSeriesWithDuplicateLabelNamesError, + MimirTooManyHAClustersError, + } +) // Metric represents a Prometheus time series metric. type Metric struct { @@ -296,19 +408,17 @@ func checkWriteError(writeErr promremote.WriteError) (err error, ignored bool) { msg := writeErr.Error() // HA may potentially write different values for the same timestamp, so we ignore this error // TODO: this may not be needed, further testing needed - for _, e := range DuplicateTimestampErrors { + for _, e := range IgnoredErrors { if strings.Contains(msg, e) { return nil, true } } // Check for expected user errors. - switch { - case strings.Contains(msg, MimirInvalidLabelError), - strings.Contains(msg, MimirMaxSeriesPerUserError), - strings.Contains(msg, MimirMaxLabelNamesPerSeriesError), - strings.Contains(msg, MimirLabelValueTooLongError): - return errors.Join(ErrRejectedWrite, writeErr), false + for _, e := range ExpectedErrors { + if strings.Contains(msg, e) { + return errors.Join(ErrRejectedWrite, writeErr), false + } } // For now, all 400s that are not previously known are considered unexpected. diff --git a/pkg/services/ngalert/writer/prom_test.go b/pkg/services/ngalert/writer/prom_test.go index c300d700d93..862cd5c339f 100644 --- a/pkg/services/ngalert/writer/prom_test.go +++ b/pkg/services/ngalert/writer/prom_test.go @@ -191,7 +191,7 @@ func TestPrometheusWriter_Write(t *testing.T) { }) t.Run("ignores client error when status code is 400 and message contains duplicate timestamp error", func(t *testing.T) { - for _, msg := range DuplicateTimestampErrors { + for _, msg := range IgnoredErrors { t.Run(msg, func(t *testing.T) { clientErr := testClientWriteError{ statusCode: http.StatusBadRequest, @@ -208,7 +208,7 @@ func TestPrometheusWriter_Write(t *testing.T) { }) t.Run("bad labels fit under the client error category", func(t *testing.T) { - msg := MimirInvalidLabelError + msg := MimirSeriesInvalidLabelError clientErr := testClientWriteError{ statusCode: http.StatusBadRequest, msg: &msg, diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 016e1e4dc26..95ab7d70af5 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -89,6 +89,7 @@ func TestLoader_Load(t *testing.T) { }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, + {Name: "Documentation", URL: "https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/"}, }, }, Includes: []*plugins.Includes{ diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 458e02694bd..f43f95c3268 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -27,7 +27,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pf finder.Finder, pr FindFunc: pf.Find, FindFilterFuncs: []discovery.FindFilterFunc{ discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{ - plugins.TypeDataSource, plugins.TypeApp, plugins.TypePanel, plugins.TypeSecretsManager, + plugins.TypeDataSource, plugins.TypeApp, plugins.TypePanel, }), func(ctx context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { return NewDuplicatePluginIDFilterStep(pr).Filter(ctx, b) diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index da0319b3d7f..afe0ae6c5b4 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -70,7 +70,6 @@ var WireSet = wire.NewSet( wire.Bind(new(pluginconfig.PluginRequestConfigProvider), new(*pluginconfig.RequestConfigProvider)), pluginstore.ProvideService, wire.Bind(new(pluginstore.Store), new(*pluginstore.Service)), - wire.Bind(new(plugins.SecretsPluginManager), new(*pluginstore.Service)), wire.Bind(new(plugins.StaticRouteResolver), new(*pluginstore.Service)), process.ProvideService, wire.Bind(new(process.Manager), new(*process.Service)), diff --git a/pkg/services/pluginsintegration/pluginstore/store.go b/pkg/services/pluginsintegration/pluginstore/store.go index e4d188572d2..20ef15379cf 100644 --- a/pkg/services/pluginsintegration/pluginstore/store.go +++ b/pkg/services/pluginsintegration/pluginstore/store.go @@ -94,15 +94,6 @@ func (s *Service) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []Pl return pluginsList } -func (s *Service) SecretsManager(ctx context.Context) *plugins.Plugin { - for _, p := range s.availablePlugins(ctx) { - if p.IsSecretsManager() { - return p - } - } - return nil -} - // plugin finds a plugin with `pluginID` from the registry that is not decommissioned func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) { p, exists := s.pluginRegistry.Plugin(ctx, pluginID, "") // version is not required since Grafana only supports single versions of a plugin diff --git a/pkg/services/pluginsintegration/pluginstore/store_test.go b/pkg/services/pluginsintegration/pluginstore/store_test.go index 587d7e6d68c..cead243e33e 100644 --- a/pkg/services/pluginsintegration/pluginstore/store_test.go +++ b/pkg/services/pluginsintegration/pluginstore/store_test.go @@ -120,7 +120,6 @@ func TestStore_Routes(t *testing.T) { t.Run("Routes returns all static routes for non-decommissioned plugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "a-test-renderer", Type: plugins.TypeRenderer}, FS: fakes.NewFakePluginFS("/some/dir")} p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.TypePanel}, FS: fakes.NewFakePluginFS("/grafana/")} - p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-secrets", Type: plugins.TypeSecretsManager}, FS: fakes.NewFakePluginFS("./secrets"), Class: plugins.ClassCore} p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-datasource", Type: plugins.TypeDataSource}, FS: fakes.NewFakePluginFS("../test")} p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-app", Type: plugins.TypeApp}, FS: fakes.NewFakePluginFS("any/path")} p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.TypeApp}} @@ -130,7 +129,6 @@ func TestStore_Routes(t *testing.T) { Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, - p3.ID: p3, p4.ID: p4, p5.ID: p5, p6.ID: p6, @@ -146,27 +144,6 @@ func TestStore_Routes(t *testing.T) { }) } -func TestStore_SecretsManager(t *testing.T) { - t.Run("Renderer returns a single (non-decommissioned) secrets manager plugin", func(t *testing.T) { - p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.TypeRenderer}} - p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.TypePanel}} - p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.TypeSecretsManager}} - p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} - - ps := New(&fakes.FakePluginRegistry{ - Store: map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - p3.ID: p3, - p4.ID: p4, - }, - }, &fakes.FakeLoader{}) - - r := ps.SecretsManager(context.Background()) - require.Equal(t, p3, r) - }) -} - func TestProcessManager_shutdown(t *testing.T) { p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} // Backend: true backend := &fakes.FakeBackendPlugin{} diff --git a/pkg/services/pluginsintegration/renderer/renderer_test.go b/pkg/services/pluginsintegration/renderer/renderer_test.go index 2f7f5d0c913..755622d2e52 100644 --- a/pkg/services/pluginsintegration/renderer/renderer_test.go +++ b/pkg/services/pluginsintegration/renderer/renderer_test.go @@ -39,7 +39,7 @@ func TestRenderer(t *testing.T) { r, exists := m.Renderer(context.Background()) require.False(t, exists) - require.Equal(t, 4, numLoaded) + require.Equal(t, 3, numLoaded) require.Equal(t, 0, numUnloaded) require.Nil(t, r) }) diff --git a/pkg/services/pluginsintegration/renderer/testdata/plugins/secrets-manager/plugin.json b/pkg/services/pluginsintegration/renderer/testdata/plugins/secrets-manager/plugin.json deleted file mode 100644 index 635a3ff47b9..00000000000 --- a/pkg/services/pluginsintegration/renderer/testdata/plugins/secrets-manager/plugin.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "secretsmanager" -} diff --git a/pkg/services/pluginsintegration/sandbox/sandbox.go b/pkg/services/pluginsintegration/sandbox/sandbox.go index 4ec5ac27c69..cf82d25e71a 100644 --- a/pkg/services/pluginsintegration/sandbox/sandbox.go +++ b/pkg/services/pluginsintegration/sandbox/sandbox.go @@ -1,9 +1,13 @@ package sandbox -import "github.com/grafana/grafana/pkg/setting" +import ( + "context" + + "github.com/grafana/grafana/pkg/setting" +) type Sandbox interface { - Plugins() ([]string, error) + Plugins(ctx context.Context) ([]string, error) } type Service struct { @@ -16,6 +20,6 @@ func ProvideService(cfg *setting.Cfg) *Service { } } -func (s *Service) Plugins() ([]string, error) { +func (s *Service) Plugins(ctx context.Context) ([]string, error) { return s.cfg.EnableFrontendSandboxForPlugins, nil } diff --git a/pkg/services/pluginsintegration/sandbox/sandbox_test.go b/pkg/services/pluginsintegration/sandbox/sandbox_test.go index b318ae19978..1b271647db2 100644 --- a/pkg/services/pluginsintegration/sandbox/sandbox_test.go +++ b/pkg/services/pluginsintegration/sandbox/sandbox_test.go @@ -1,6 +1,7 @@ package sandbox import ( + "context" "testing" "github.com/grafana/grafana/pkg/setting" @@ -13,7 +14,7 @@ func TestService_Plugins(t *testing.T) { } service := ProvideService(cfg) - plugins, err := service.Plugins() + plugins, err := service.Plugins(context.Background()) assert.NoError(t, err) assert.Equal(t, []string{"plugin1", "plugin2"}, plugins) } diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 8c27d18b0ea..ddbf2925d4b 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -21,8 +21,11 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/infra/tracing" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/apiserver/client" @@ -330,6 +333,8 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(db, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore(), ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 1ae4797a45f..e8ea78749a6 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -19,6 +19,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -29,7 +31,6 @@ import ( dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" . "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -1392,7 +1393,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { testDB, cfg := db.InitTestDBWithCfg(t) dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB)) require.NoError(t, err) - ac := acmock.New() + ac := actest.FakeAccessControl{ExpectedEvaluate: true} fStore := folderimpl.ProvideStore(testDB) folderPermissions := acmock.NewMockedPermissionsService() @@ -1401,15 +1402,11 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) - dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(testDB, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) - fakeGuardian := &guardian.FakeDashboardGuardian{ - CanSaveValue: true, - CanEditUIDs: []string{}, - CanViewUIDs: []string{}, - } - guardian.MockDashboardGuardian(fakeGuardian) // insert in test data so we can check that permissions are working properly through the dashboard service // this will create 4 dashboards and 3 users diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 6100f1f0e6a..c8cc6d5dfde 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -12,7 +12,9 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -499,7 +501,9 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), + kvstore.NewFakeKVStore()) require.NoError(t, err) dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) diff --git a/pkg/services/secrets/kvstore/kvstore.go b/pkg/services/secrets/kvstore/kvstore.go index 50a37421f45..2025c7c3118 100644 --- a/pkg/services/secrets/kvstore/kvstore.go +++ b/pkg/services/secrets/kvstore/kvstore.go @@ -5,13 +5,8 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets" - "github.com/grafana/grafana/pkg/setting" ) const ( @@ -22,45 +17,8 @@ const ( func ProvideService( sqlStore db.DB, secretsService secrets.Service, - pluginsManager plugins.SecretsPluginManager, - kvstore kvstore.KVStore, - features featuremgmt.FeatureToggles, - cfg *setting.Cfg, ) (SecretsKVStore, error) { - var logger = log.New("secrets.kvstore") - var store SecretsKVStore - ctx := context.Background() - store = NewSQLSecretsKVStore(sqlStore, secretsService, logger) - err := EvaluateRemoteSecretsPlugin(ctx, pluginsManager, cfg) - if err != nil { - logger.Debug("secrets manager evaluator returned false", "reason", err.Error()) - } else { - // Attempt to start the plugin - var secretsPlugin secretsmanagerplugin.SecretsManagerPlugin - secretsPlugin, err = StartAndReturnPlugin(pluginsManager, ctx) - namespacedKVStore := GetNamespacedKVStore(kvstore) - if err != nil || secretsPlugin == nil { - logger.Error("failed to start remote secrets management plugin") - if isFatal, readErr := IsPluginStartupErrorFatal(ctx, namespacedKVStore); isFatal || readErr != nil { - // plugin error was fatal or there was an error determining if the error was fatal - logger.Error("secrets management plugin is required to start -- exiting app") - if readErr != nil { - return nil, readErr - } - return nil, err - } - } else { - // as the plugin is installed, SecretsKVStoreSQL is now replaced with - // an instance of SecretsKVStorePlugin with the sql store as a fallback - // (used for migration and in case a secret is not found). - store = NewPluginSecretsKVStore(secretsPlugin, secretsService, namespacedKVStore, features, WithCache(store, 5*time.Second, 5*time.Minute), logger) - } - } - - if err != nil { - logger.Debug("secrets kvstore is using the default (SQL) implementation for secrets management") - } - + store := NewSQLSecretsKVStore(sqlStore, secretsService, log.New("secrets.kvstore")) return WithCache(store, 5*time.Second, 5*time.Minute), nil } diff --git a/pkg/services/secrets/kvstore/migrations/from_plugin_mig.go b/pkg/services/secrets/kvstore/migrations/from_plugin_mig.go deleted file mode 100644 index d8232e9b229..00000000000 --- a/pkg/services/secrets/kvstore/migrations/from_plugin_mig.go +++ /dev/null @@ -1,108 +0,0 @@ -package migrations - -import ( - "context" - "errors" - "fmt" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - "github.com/grafana/grafana/pkg/services/secrets" - secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" - "github.com/grafana/grafana/pkg/setting" -) - -var errPluginUnavailable = errors.New("remote secret management plugin is unavailable") - -// MigrateFromPluginService This migrator will handle migration of the configured plugin secrets back to Grafana unified secrets -type MigrateFromPluginService struct { - cfg *setting.Cfg - sqlStore db.DB - secretsService secrets.Service - manager plugins.SecretsPluginManager - kvstore kvstore.KVStore -} - -func ProvideMigrateFromPluginService( - cfg *setting.Cfg, - sqlStore db.DB, - secretsService secrets.Service, - manager plugins.SecretsPluginManager, - kvstore kvstore.KVStore, - -) *MigrateFromPluginService { - return &MigrateFromPluginService{ - cfg: cfg, - sqlStore: sqlStore, - secretsService: secretsService, - manager: manager, - kvstore: kvstore, - } -} - -func (s *MigrateFromPluginService) Migrate(ctx context.Context) error { - logger.Debug("starting migration of plugin secrets to unified secrets") - // access the plugin directly - plugin, err := secretskvs.StartAndReturnPlugin(s.manager, context.Background()) - if err != nil { - return errPluginUnavailable - } - // Get full list of secrets from the plugin - res, err := plugin.GetAllSecrets(ctx, &secretsmanagerplugin.GetAllSecretsRequest{}) - if err != nil { - logger.Error("Failed to retrieve all secrets from plugin") - return err - } - totalSecrets := len(res.Items) - logger.Debug("retrieved all secrets from plugin", "num secrets", totalSecrets) - // create a secret sql store manually - secretsSql := secretskvs.NewSQLSecretsKVStore(s.sqlStore, s.secretsService, logger) - for i, item := range res.Items { - logger.Debug(fmt.Sprintf("Migrating secret %d of %d", i+1, totalSecrets), "current", i+1, "secretCount", totalSecrets) - // Add to sql store - err = secretsSql.Set(ctx, item.Key.OrgId, item.Key.Namespace, item.Key.Type, item.Value) - if err != nil { - logger.Error("Error adding secret to unified secrets", "orgId", item.Key.OrgId, - "namespace", item.Key.Namespace, "type", item.Key.Type) - return err - } - } - - for i, item := range res.Items { - logger.Debug(fmt.Sprintf("Cleaning secret %d of %d", i+1, totalSecrets), "current", i+1, "secretCount", totalSecrets) - // Delete from the plugin - _, err := plugin.DeleteSecret(ctx, &secretsmanagerplugin.DeleteSecretRequest{ - KeyDescriptor: &secretsmanagerplugin.Key{ - OrgId: item.Key.OrgId, - Namespace: item.Key.Namespace, - Type: item.Key.Type, - }}) - if err != nil { - logger.Error("Error deleting secret from plugin after migration", "orgId", item.Key.OrgId, - "namespace", item.Key.Namespace, "type", item.Key.Type) - continue - } - } - logger.Debug("Completed migration of secrets from plugin") - - // The plugin is no longer needed at the moment - err = secretskvs.SetPluginStartupErrorFatal(ctx, secretskvs.GetNamespacedKVStore(s.kvstore), false) - if err != nil { - logger.Error("Failed to remove plugin error fatal flag", "error", err.Error()) - } - // Reset the fatal flag setter in case another secret is created on the plugin - secretskvs.ResetPlugin() - - logger.Debug("Shutting down secrets plugin now that migration is complete") - // if `use_plugin` wasn't set, stop the plugin after migration - if !s.cfg.SectionWithEnvOverrides("secrets").Key("use_plugin").MustBool(false) { - err := s.manager.SecretsManager(ctx).Stop(ctx) - if err != nil { - // Log a warning but don't throw an error - logger.Error("Error stopping secrets plugin after migration", "error", err.Error()) - } - } - return nil -} diff --git a/pkg/services/secrets/kvstore/migrations/from_plugin_mig_test.go b/pkg/services/secrets/kvstore/migrations/from_plugin_mig_test.go deleted file mode 100644 index 7181fa168ac..00000000000 --- a/pkg/services/secrets/kvstore/migrations/from_plugin_mig_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package migrations - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - "github.com/grafana/grafana/pkg/services/secrets/fakes" - secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" - secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/setting" -) - -// This tests will create a mock sql database and an inmemory -// implementation of the secret manager to simulate the plugin. -func TestPluginSecretMigrationService_MigrateFromPlugin(t *testing.T) { - ctx := context.Background() - - t.Run("migrate secrets from secrets plugin to Grafana", func(t *testing.T) { - // --- SETUP - migratorService, plugin, sqlStore := setupTestMigrateFromPluginService(t) - - addSecretToPluginStore(t, plugin, ctx, 1, "secret-1", "bogus", "value-1") - addSecretToPluginStore(t, plugin, ctx, 1, "secret-2", "bogus", "value-2") - - // --- EXECUTION - err := migratorService.Migrate(ctx) - require.NoError(t, err) - - // --- VALIDATIONS - validatePluginSecretsWereDeleted(t, plugin, ctx) - - validateSecretWasStoredInSql(t, sqlStore, ctx, 1, "secret-1", "bogus", "value-1") - validateSecretWasStoredInSql(t, sqlStore, ctx, 1, "secret-2", "bogus", "value-2") - }) -} - -// Set up services used in migration -func setupTestMigrateFromPluginService(t *testing.T) (*MigrateFromPluginService, secretsmanagerplugin.SecretsManagerPlugin, *secretskvs.SecretsKVStoreSQL) { - t.Helper() - - // this is to init the sql secret store inside the migration - sqlStore := db.InitTestDB(t) - secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - manager := secretskvs.NewFakeSecretsPluginManager(t, false) - migratorService := ProvideMigrateFromPluginService( - setting.NewCfg(), - sqlStore, - secretsService, - manager, - kvstore.ProvideService(sqlStore), - ) - - secretsSql := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - - return migratorService, manager.SecretsManager(context.Background()).SecretsManager, secretsSql -} - -func addSecretToPluginStore(t *testing.T, plugin secretsmanagerplugin.SecretsManagerPlugin, ctx context.Context, orgId int64, namespace string, typ string, value string) { - t.Helper() - _, err := plugin.SetSecret(ctx, &secretsmanagerplugin.SetSecretRequest{ - KeyDescriptor: &secretsmanagerplugin.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - Value: value, - }) - require.NoError(t, err) -} - -// validates that secrets on the plugin were deleted -func validatePluginSecretsWereDeleted(t *testing.T, plugin secretsmanagerplugin.SecretsManagerPlugin, ctx context.Context) { - t.Helper() - res, err := plugin.GetAllSecrets(ctx, &secretsmanagerplugin.GetAllSecretsRequest{}) - require.NoError(t, err) - require.Equal(t, 0, len(res.Items)) -} - -// validates that secrets are in sql -func validateSecretWasStoredInSql(t *testing.T, sqlStore *secretskvs.SecretsKVStoreSQL, ctx context.Context, orgId int64, namespace string, typ string, expectedValue string) { - t.Helper() - res, exists, err := sqlStore.Get(ctx, orgId, namespace, typ) - require.NoError(t, err) - require.True(t, exists) - require.Equal(t, expectedValue, res) -} diff --git a/pkg/services/secrets/kvstore/migrations/migrator.go b/pkg/services/secrets/kvstore/migrations/migrator.go index 3c2671327ca..7cfce10183a 100644 --- a/pkg/services/secrets/kvstore/migrations/migrator.go +++ b/pkg/services/secrets/kvstore/migrations/migrator.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/setting" ) var logger = log.New("secret.migration") @@ -22,40 +21,20 @@ type SecretMigrationService interface { type SecretMigrationProvider interface { registry.BackgroundService - TriggerPluginMigration(ctx context.Context, toPlugin bool) error } type SecretMigrationProviderImpl struct { - services []SecretMigrationService - ServerLockService *serverlock.ServerLockService - migrateToPluginService *MigrateToPluginService - migrateFromPluginService *MigrateFromPluginService + services []SecretMigrationService + ServerLockService *serverlock.ServerLockService } func ProvideSecretMigrationProvider( - cfg *setting.Cfg, serverLockService *serverlock.ServerLockService, dataSourceSecretMigrationService *DataSourceSecretMigrationService, - migrateToPluginService *MigrateToPluginService, - migrateFromPluginService *MigrateFromPluginService, ) *SecretMigrationProviderImpl { - services := make([]SecretMigrationService, 0) - services = append(services, dataSourceSecretMigrationService) - // Plugin migration should always be last; should either migrate to or from, not both - // This is because the migrateTo checks for use_plugin = true, in which case we should always - // migrate by default to ensure users don't lose access to secrets. If migration has - // already occurred, the migrateTo function will be called but it won't do anything - if cfg.SectionWithEnvOverrides("secrets").Key("migrate_from_plugin").MustBool(false) { - services = append(services, migrateFromPluginService) - } else { - services = append(services, migrateToPluginService) - } - return &SecretMigrationProviderImpl{ - ServerLockService: serverLockService, - services: services, - migrateToPluginService: migrateToPluginService, - migrateFromPluginService: migrateFromPluginService, + ServerLockService: serverLockService, + services: []SecretMigrationService{dataSourceSecretMigrationService}, } } @@ -83,23 +62,3 @@ func (s *SecretMigrationProviderImpl) Migrate(ctx context.Context) error { } return nil } - -// TriggerPluginMigration Kick off a migration to or from the plugin. This will block until all services have exited. -func (s *SecretMigrationProviderImpl) TriggerPluginMigration(ctx context.Context, toPlugin bool) error { - // Don't migrate if there is already one happening - return s.ServerLockService.LockExecuteAndRelease(ctx, actionName, time.Minute*10, func(context.Context) { - var err error - if toPlugin { - err = s.migrateToPluginService.Migrate(ctx) - } else { - err = s.migrateFromPluginService.Migrate(ctx) - } - if err != nil { - direction := "from_plugin" - if toPlugin { - direction = "to_plugin" - } - logger.Error("Failed to migrate plugin secrets", "direction", direction, "error", err.Error()) - } - }) -} diff --git a/pkg/services/secrets/kvstore/migrations/to_plugin_mig.go b/pkg/services/secrets/kvstore/migrations/to_plugin_mig.go deleted file mode 100644 index 5a0f0d4047b..00000000000 --- a/pkg/services/secrets/kvstore/migrations/to_plugin_mig.go +++ /dev/null @@ -1,120 +0,0 @@ -package migrations - -import ( - "context" - "errors" - "fmt" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/secrets" - secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" - "github.com/grafana/grafana/pkg/setting" -) - -var errSecretStoreIsNotPlugin = errors.New("SecretsKVStore is not a SecretsKVStorePlugin") - -// MigrateToPluginService This migrator will handle migration of datasource secrets (aka Unified secrets) -// into the plugin secrets configured -type MigrateToPluginService struct { - secretsStore secretskvs.SecretsKVStore - cfg *setting.Cfg - sqlStore db.DB - secretsService secrets.Service - kvstore kvstore.KVStore - manager plugins.SecretsPluginManager -} - -func ProvideMigrateToPluginService( - secretsStore secretskvs.SecretsKVStore, - cfg *setting.Cfg, - sqlStore db.DB, - secretsService secrets.Service, - kvstore kvstore.KVStore, - manager plugins.SecretsPluginManager, -) *MigrateToPluginService { - return &MigrateToPluginService{ - secretsStore: secretsStore, - cfg: cfg, - sqlStore: sqlStore, - secretsService: secretsService, - kvstore: kvstore, - manager: manager, - } -} - -func (s *MigrateToPluginService) Migrate(ctx context.Context) error { - err := secretskvs.EvaluateRemoteSecretsPlugin(ctx, s.manager, s.cfg) - hasStarted := secretskvs.HasPluginStarted(ctx, s.manager) - if err == nil && hasStarted { - logger.Debug("starting migration of unified secrets to the plugin") - // we need to get the fallback store since in this scenario the secrets store would be the plugin. - tmpStore, err := secretskvs.GetUnwrappedStoreFromCache(s.secretsStore) - if err != nil { - tmpStore = s.secretsStore - logger.Warn("secret store is not cached, this is unexpected - continuing migration anyway.") - } - pluginStore, ok := tmpStore.(*secretskvs.SecretsKVStorePlugin) - if !ok { - return errSecretStoreIsNotPlugin - } - fallbackStore := pluginStore.Fallback() - - // before we start migrating, check see if plugin startup failures were already fatal - namespacedKVStore := secretskvs.GetNamespacedKVStore(s.kvstore) - wasFatal, err := secretskvs.IsPluginStartupErrorFatal(ctx, namespacedKVStore) - if err != nil { - logger.Warn("unable to determine whether plugin startup failures are fatal - continuing migration anyway.") - } - - var allSec []secretskvs.Item - var totalSec int - // during migration we need to have fallback enabled while we move secrets to plugin - err = pluginStore.WithFallbackEnabled(func() error { - // get all secrets in the fallback store - allSec, err = fallbackStore.GetAll(ctx) - if err != nil { - return nil - } - totalSec := len(allSec) - logger.Debug(fmt.Sprintf("Total amount of secrets to migrate: %d", totalSec)) - - // We just set it again as the current secret store should be the plugin secret - for i, sec := range allSec { - logger.Debug(fmt.Sprintf("Migrating secret %d of %d", i+1, totalSec), "current", i+1, "secretCount", totalSec) - err = pluginStore.Set(ctx, *sec.OrgId, *sec.Namespace, *sec.Type, sec.Value) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - - // as no err was returned, when we delete all the secrets from the sql store - logger.Debug("migrated unified secrets to plugin", "number of secrets", totalSec) - for index, sec := range allSec { - logger.Debug(fmt.Sprintf("Cleaning secret %d of %d", index+1, totalSec), "current", index+1, "secretCount", totalSec) - - err = fallbackStore.Del(ctx, *sec.OrgId, *sec.Namespace, *sec.Type) - if err != nil { - logger.Error("plugin migrator encountered error while deleting unified secrets") - if index == 0 && !wasFatal { - // old unified secrets still exists, so plugin startup errors are still not fatal, unless they were before we started - err := secretskvs.SetPluginStartupErrorFatal(ctx, namespacedKVStore, false) - if err != nil { - logger.Error("error reverting plugin failure fatal status", "error", err.Error()) - } else { - logger.Debug("application will continue to function without the secrets plugin") - } - } - return err - } - } - logger.Debug("deleted unified secrets after migration", "number of secrets", totalSec) - } - return nil -} diff --git a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go b/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go deleted file mode 100644 index b2ad6dbf956..00000000000 --- a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package migrations - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/secrets/fakes" - secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" - secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/setting" -) - -// This tests will create a mock sql database and an inmemory -// implementation of the secret manager to simulate the plugin. -func TestPluginSecretMigrationService_MigrateToPlugin(t *testing.T) { - ctx := context.Background() - - t.Run("migration run ok - 2 secrets migrated", func(t *testing.T) { - // --- SETUP - migratorService, secretsStore, sqlSecretStore := setupTestMigrateToPluginService(t) - var orgId int64 = 1 - namespace1, namespace2 := "namespace-test", "namespace-test2" - typ := "type-test" - value := "SUPER_SECRET" - - addSecretToSqlStore(t, sqlSecretStore, ctx, orgId, namespace1, typ, value) - addSecretToSqlStore(t, sqlSecretStore, ctx, orgId, namespace2, typ, value) - - // --- EXECUTION - err := migratorService.Migrate(ctx) - require.NoError(t, err) - - // --- VALIDATIONS - validateSqlSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace1, typ) - validateSqlSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace2, typ) - - validateSecretWasStoredInPlugin(t, secretsStore, ctx, orgId, namespace1, typ) - validateSecretWasStoredInPlugin(t, secretsStore, ctx, orgId, namespace1, typ) - }) -} - -// With fatal flag unset, do a migration with backwards compatibility disabled. When unified secrets are deleted, return an error on the first deletion -// Should result in the fatal flag remaining unset -func TestFatalPluginErr_MigrationTestWithErrorDeletingUnifiedSecrets(t *testing.T) { - p, err := secretskvs.SetupFatalCrashTest(t, false, false, true) - assert.NoError(t, err) - - migration := setupTestMigratorServiceWithDeletionError(t, p.SecretsKVStore, &dbtest.FakeDB{ - ExpectedError: errors.New("random error"), - }, p.KVStore) - err = migration.Migrate(context.Background()) - assert.Error(t, err) - assert.Equal(t, "mocked del error", err.Error()) - - isFatal, err := secretskvs.IsPluginStartupErrorFatal(context.Background(), secretskvs.GetNamespacedKVStore(p.KVStore)) - assert.NoError(t, err) - assert.False(t, isFatal) -} - -func addSecretToSqlStore(t *testing.T, sqlSecretStore secretskvs.SecretsKVStore, ctx context.Context, orgId int64, namespace1 string, typ string, value string) { - t.Helper() - err := sqlSecretStore.Set(ctx, orgId, namespace1, typ, value) - require.NoError(t, err) -} - -// validates that secrets on the sql store were deleted. -func validateSqlSecretWasDeleted(t *testing.T, sqlSecretStore secretskvs.SecretsKVStore, ctx context.Context, orgId int64, namespace1 string, typ string) { - t.Helper() - res, err := sqlSecretStore.Keys(ctx, orgId, namespace1, typ) - require.NoError(t, err) - require.Equal(t, 0, len(res)) -} - -// validates that secrets should be on the plugin -func validateSecretWasStoredInPlugin(t *testing.T, secretsStore secretskvs.SecretsKVStore, ctx context.Context, orgId int64, namespace1 string, typ string) { - t.Helper() - resPlugin, err := secretsStore.Keys(ctx, orgId, namespace1, typ) - require.NoError(t, err) - require.Equal(t, 1, len(resPlugin)) -} - -// Set up services used in migration -func setupTestMigrateToPluginService(t *testing.T) (*MigrateToPluginService, secretskvs.SecretsKVStore, secretskvs.SecretsKVStore) { - t.Helper() - - rawCfg := ` - [secrets] - use_plugin = true - ` - raw, err := ini.Load([]byte(rawCfg)) - require.NoError(t, err) - cfg := &setting.Cfg{Raw: raw} - sqlStore := db.InitTestDB(t) - - // this would be the plugin - mocked at the moment - fallbackStore := secretskvs.WithCache(secretskvs.NewFakeSQLSecretsKVStore(t, sqlStore), time.Minute*5, time.Minute*5) - secretsStoreForPlugin := secretskvs.WithCache(secretskvs.NewFakePluginSecretsKVStore(t, featuremgmt.WithFeatures(), fallbackStore), time.Minute*5, time.Minute*5) - - // this is to init the sql secret store inside the migration - secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - manager := secretskvs.NewFakeSecretsPluginManager(t, false) - migratorService := ProvideMigrateToPluginService( - secretsStoreForPlugin, - cfg, - sqlStore, - secretsService, - kvstore.ProvideService(sqlStore), - manager, - ) - - return migratorService, secretsStoreForPlugin, fallbackStore -} - -func setupTestMigratorServiceWithDeletionError( - t *testing.T, - secretskv secretskvs.SecretsKVStore, - sqlStore db.DB, - kvstore kvstore.KVStore, -) *MigrateToPluginService { - t.Helper() - t.Cleanup(secretskvs.ResetPlugin) - cfg := secretskvs.SetupTestConfig(t) - secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - manager := secretskvs.NewFakeSecretsPluginManager(t, false) - migratorService := ProvideMigrateToPluginService( - secretskv, - cfg, - sqlStore, - secretsService, - kvstore, - manager, - ) - fallback := secretskvs.NewFakeSecretsKVStore() - var orgId int64 = 1 - str := "random string" - err := fallback.Set(context.Background(), orgId, str, str, "bogus") - require.NoError(t, err) - fallback.DeletionError(true) - err = secretskvs.ReplaceFallback(t, secretskv, fallback) - require.NoError(t, err) - return migratorService -} diff --git a/pkg/services/secrets/kvstore/model.go b/pkg/services/secrets/kvstore/model.go index 463fd7c2563..c89869d0afb 100644 --- a/pkg/services/secrets/kvstore/model.go +++ b/pkg/services/secrets/kvstore/model.go @@ -5,9 +5,7 @@ import ( ) const ( - QuitOnPluginStartupFailureKey = "quit_on_secrets_plugin_startup_failure" - PluginNamespace = "secretsmanagerplugin" - DataSourceSecretType = "datasource" + DataSourceSecretType = "datasource" ) // Item stored in k/v store. diff --git a/pkg/services/secrets/kvstore/plugin.go b/pkg/services/secrets/kvstore/plugin.go deleted file mode 100644 index a944da822f2..00000000000 --- a/pkg/services/secrets/kvstore/plugin.go +++ /dev/null @@ -1,288 +0,0 @@ -package kvstore - -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" - smp "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/secrets" - "github.com/grafana/grafana/pkg/setting" -) - -var ( - fatalFlagOnce sync.Once - startupOnce sync.Once - errPluginDisabledByConfig = errors.New("remote secret management plugin disabled because the property `secrets.use_plugin` is not set to `true`") - errPluginNotInstalled = errors.New("remote secret management plugin disabled because there is no installed plugin of type `secretsmanager`") -) - -// SecretsKVStorePlugin provides a key/value store backed by the Grafana plugin gRPC interface -type SecretsKVStorePlugin struct { - sync.Mutex - log log.Logger - secretsPlugin smp.SecretsManagerPlugin - secretsService secrets.Service - kvstore *kvstore.NamespacedKVStore - backwardsCompatibilityDisabled bool - fallbackEnabled bool - fallbackStore SecretsKVStore -} - -func NewPluginSecretsKVStore( - secretsPlugin smp.SecretsManagerPlugin, - secretsService secrets.Service, - kvstore *kvstore.NamespacedKVStore, - features featuremgmt.FeatureToggles, - fallback SecretsKVStore, - logger log.Logger, -) *SecretsKVStorePlugin { - return &SecretsKVStorePlugin{ - secretsPlugin: secretsPlugin, - secretsService: secretsService, - log: logger, - kvstore: kvstore, - backwardsCompatibilityDisabled: features.IsEnabledGlobally(featuremgmt.FlagDisableSecretsCompatibility), - fallbackStore: fallback, - } -} - -// Get an item from the store -// If it is the first time a secret has been retrieved and backwards compatibility is disabled, mark plugin startup errors fatal -func (kv *SecretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) { - req := &smp.GetSecretRequest{ - KeyDescriptor: &smp.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - } - - res, err := kv.secretsPlugin.GetSecret(ctx, req) - if res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - if res.Exists { - updateFatalFlag(ctx, kv) - } - - if kv.fallbackEnabled { - if err != nil || res.UserFriendlyError != "" || !res.Exists { - res.DecryptedValue, res.Exists, err = kv.fallbackStore.Get(ctx, orgId, namespace, typ) - } - } - - return res.DecryptedValue, res.Exists, err -} - -// Set an item in the store -// If it is the first time a secret has been set and backwards compatibility is disabled, mark plugin startup errors fatal -func (kv *SecretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error { - req := &smp.SetSecretRequest{ - KeyDescriptor: &smp.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - Value: value, - } - - res, err := kv.secretsPlugin.SetSecret(ctx, req) - if err == nil && res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - updateFatalFlag(ctx, kv) - - return err -} - -// Del deletes an item from the store. -func (kv *SecretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace string, typ string) error { - req := &smp.DeleteSecretRequest{ - KeyDescriptor: &smp.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - } - - res, err := kv.secretsPlugin.DeleteSecret(ctx, req) - if err == nil && res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - return err -} - -// Keys get all keys for a given namespace. To query for all -// organizations the constant 'kvstore.AllOrganizations' can be passed as orgId. -func (kv *SecretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) { - req := &smp.ListSecretsRequest{ - KeyDescriptor: &smp.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - AllOrganizations: orgId == AllOrganizations, - } - - res, err := kv.secretsPlugin.ListSecrets(ctx, req) - if err != nil { - return nil, err - } else if res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - return parseKeys(res.Keys), err -} - -// Rename an item in the store -func (kv *SecretsKVStorePlugin) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error { - req := &smp.RenameSecretRequest{ - KeyDescriptor: &smp.Key{ - OrgId: orgId, - Namespace: namespace, - Type: typ, - }, - NewNamespace: newNamespace, - } - - res, err := kv.secretsPlugin.RenameSecret(ctx, req) - if err == nil && res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - return err -} - -func (kv *SecretsKVStorePlugin) GetAll(ctx context.Context) ([]Item, error) { - req := &smp.GetAllSecretsRequest{} - - res, err := kv.secretsPlugin.GetAllSecrets(ctx, req) - if err != nil { - return nil, err - } else if res.UserFriendlyError != "" { - err = wrapUserFriendlySecretError(res.UserFriendlyError) - } - - return parseItems(res.Items), err -} - -func (kv *SecretsKVStorePlugin) Fallback() SecretsKVStore { - return kv.fallbackStore -} - -func (kv *SecretsKVStorePlugin) WithFallbackEnabled(fn func() error) error { - kv.Lock() - defer kv.Unlock() - kv.fallbackEnabled = true - err := fn() - kv.fallbackEnabled = false - return err -} - -func parseKeys(keys []*smp.Key) []Key { - newKeys := make([]Key, 0, len(keys)) - - for _, k := range keys { - newKey := Key{OrgId: k.OrgId, Namespace: k.Namespace, Type: k.Type} - newKeys = append(newKeys, newKey) - } - - return newKeys -} - -func parseItems(items []*smp.Item) []Item { - newItems := make([]Item, 0, len(items)) - - for _, i := range items { - newItem := Item{OrgId: &i.Key.OrgId, Namespace: &i.Key.Namespace, Type: &i.Key.Type, Value: i.Value} - newItems = append(newItems, newItem) - } - - return newItems -} - -func updateFatalFlag(ctx context.Context, skv *SecretsKVStorePlugin) { - // This function makes the most sense in here because it handles all possible scenarios: - // - User changed backwards compatibility flag, so we have to migrate secrets either to or from the plugin (get or set) - // - Migration is on, so we migrate secrets to the plugin (set) - // - User doesn't migrate, but stores a new secret in the plugin (set) - // Rather than updating the flag in several places, it is cleaner to just do this check once - // Very early on. Once backwards compatibility to legacy secrets is gone in Grafana 10, this can go away as well - fatalFlagOnce.Do(func() { - skv.log.Debug("Updating plugin startup error fatal flag") - var err error - if isFatal, _ := IsPluginStartupErrorFatal(ctx, skv.kvstore); !isFatal && skv.backwardsCompatibilityDisabled { - err = SetPluginStartupErrorFatal(ctx, skv.kvstore, true) - } else if isFatal && !skv.backwardsCompatibilityDisabled { - err = SetPluginStartupErrorFatal(ctx, skv.kvstore, false) - } - if err != nil { - skv.log.Error("failed to set plugin error fatal flag", err.Error()) - } - }) -} - -func wrapUserFriendlySecretError(ufe string) datasources.ErrDatasourceSecretsPluginUserFriendly { - return datasources.ErrDatasourceSecretsPluginUserFriendly{Err: ufe} -} - -func GetNamespacedKVStore(kv kvstore.KVStore) *kvstore.NamespacedKVStore { - return kvstore.WithNamespace(kv, kvstore.AllOrganizations, PluginNamespace) -} - -func IsPluginStartupErrorFatal(ctx context.Context, kvstore *kvstore.NamespacedKVStore) (bool, error) { - _, exists, err := kvstore.Get(ctx, QuitOnPluginStartupFailureKey) - if err != nil { - return false, fmt.Errorf("error retrieving key %s from kvstore. error: %w", QuitOnPluginStartupFailureKey, err) - } - return exists, nil -} - -func SetPluginStartupErrorFatal(ctx context.Context, kvstore *kvstore.NamespacedKVStore, isFatal bool) error { - if !isFatal { - return kvstore.Del(ctx, QuitOnPluginStartupFailureKey) - } - return kvstore.Set(ctx, QuitOnPluginStartupFailureKey, "true") -} - -func EvaluateRemoteSecretsPlugin(ctx context.Context, mg plugins.SecretsPluginManager, cfg *setting.Cfg) error { - usePlugin := cfg.SectionWithEnvOverrides("secrets").Key("use_plugin").MustBool() - if !usePlugin { - return errPluginDisabledByConfig - } - pluginInstalled := mg.SecretsManager(ctx) != nil - if !pluginInstalled { - return errPluginNotInstalled - } - return nil -} - -func HasPluginStarted(ctx context.Context, mg plugins.SecretsPluginManager) bool { - return mg.SecretsManager(ctx) != nil && mg.SecretsManager(ctx).SecretsManager != nil -} - -func StartAndReturnPlugin(mg plugins.SecretsPluginManager, ctx context.Context) (smp.SecretsManagerPlugin, error) { - var err error - startupOnce.Do(func() { - err = mg.SecretsManager(ctx).Start(ctx) - }) - if err != nil { - return nil, err - } - return mg.SecretsManager(ctx).SecretsManager, nil -} - -func ResetPlugin() { - fatalFlagOnce = sync.Once{} - startupOnce = sync.Once{} -} diff --git a/pkg/services/secrets/kvstore/plugin_test.go b/pkg/services/secrets/kvstore/plugin_test.go deleted file mode 100644 index 8cd70129df6..00000000000 --- a/pkg/services/secrets/kvstore/plugin_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package kvstore - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -// Set fatal flag to true, then simulate a plugin start failure -// Should result in an error from the secret store provider -func TestFatalPluginErr_PluginFailsToStartWithFatalFlagSet(t *testing.T) { - p, err := SetupFatalCrashTest(t, true, true, false) - assert.Error(t, err) - assert.Equal(t, "mocked failed to start", err.Error()) - assert.Nil(t, p.SecretsKVStore) -} - -// Set fatal flag to false, then simulate a plugin start failure -// Should result in the secret store provider returning the sql impl -func TestFatalPluginErr_PluginFailsToStartWithFatalFlagNotSet(t *testing.T) { - p, err := SetupFatalCrashTest(t, true, false, false) - assert.NoError(t, err) - require.IsType(t, &CachedKVStore{}, p.SecretsKVStore) - - cachedKv, _ := p.SecretsKVStore.(*CachedKVStore) - store, err := GetUnwrappedStoreFromCache(cachedKv) - require.NoError(t, err) - assert.IsType(t, &SecretsKVStoreSQL{}, store) -} - -// With fatal flag not set, store a secret in the plugin while backwards compatibility is disabled -// Should result in the fatal flag going from unset -> set to true -func TestFatalPluginErr_FatalFlagGetsSetWithBackwardsCompatDisabled(t *testing.T) { - p, err := SetupFatalCrashTest(t, false, false, true) - assert.NoError(t, err) - require.NotNil(t, p.SecretsKVStore) - - err = p.SecretsKVStore.Set(context.Background(), 0, "datasource", "postgres", "my secret") - assert.NoError(t, err) - - isFatal, err := IsPluginStartupErrorFatal(context.Background(), GetNamespacedKVStore(p.KVStore)) - assert.NoError(t, err) - assert.True(t, isFatal) -} - -// With fatal flag set, retrieve a secret from the plugin while backwards compatibility is enabled -// Should result in the fatal flag going from set to true -> unset -func TestFatalPluginErr_FatalFlagGetsUnSetWithBackwardsCompatEnabled(t *testing.T) { - p, err := SetupFatalCrashTest(t, false, true, false) - assert.NoError(t, err) - require.NotNil(t, p.SecretsKVStore) - - // setup - store secret and manually bypassing the remote plugin impl - _, err = p.PluginManager.SecretsManager(context.Background()).SecretsManager.SetSecret(context.Background(), &secretsmanagerplugin.SetSecretRequest{ - KeyDescriptor: &secretsmanagerplugin.Key{ - OrgId: 0, - Namespace: "postgres", - Type: "datasource", - }, - Value: "bogus", - }) - assert.NoError(t, err) - - // retrieve the secret and check values - val, exists, err := p.SecretsKVStore.Get(context.Background(), 0, "postgres", "datasource") - assert.NoError(t, err) - assert.NotNil(t, val) - assert.True(t, exists) - - isFatal, err := IsPluginStartupErrorFatal(context.Background(), GetNamespacedKVStore(p.KVStore)) - assert.NoError(t, err) - assert.False(t, isFatal) -} diff --git a/pkg/services/secrets/kvstore/sql_test.go b/pkg/services/secrets/kvstore/sql_test.go index bb73e7d80bf..eeee727b9f0 100644 --- a/pkg/services/secrets/kvstore/sql_test.go +++ b/pkg/services/secrets/kvstore/sql_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/tests/testsuite" ) type TestCase struct { @@ -25,6 +26,10 @@ func (t *TestCase) Value() string { return fmt.Sprintf("%d:%s:%s:%d", t.OrgId, t.Namespace, t.Type, t.Revision) } +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestSecretsKVStoreSQL(t *testing.T) { sqlStore := db.InitTestDB(t) secretsService := manager.SetupTestService(t, fakes.NewFakeSecretsStore()) diff --git a/pkg/services/secrets/kvstore/test_helpers.go b/pkg/services/secrets/kvstore/test_helpers.go index 7c19def969c..479bb5d9172 100644 --- a/pkg/services/secrets/kvstore/test_helpers.go +++ b/pkg/services/secrets/kvstore/test_helpers.go @@ -3,25 +3,13 @@ package kvstore import ( "context" "errors" - "sync" "testing" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "gopkg.in/ini.v1" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" - pluginsLogger "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" ) func NewFakeSQLSecretsKVStore(t *testing.T, sqlStore *sqlstore.SQLStore) *SecretsKVStoreSQL { @@ -30,17 +18,6 @@ func NewFakeSQLSecretsKVStore(t *testing.T, sqlStore *sqlstore.SQLStore) *Secret return NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) } -func NewFakePluginSecretsKVStore(t *testing.T, features featuremgmt.FeatureToggles, fallback SecretsKVStore) *SecretsKVStorePlugin { - t.Helper() - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - store := kvstore.ProvideService(sqlStore) - namespacedKVStore := GetNamespacedKVStore(store) - manager := NewFakeSecretsPluginManager(t, false) - plugin := manager.SecretsManager(context.Background()).SecretsManager - return NewPluginSecretsKVStore(plugin, secretsService, namespacedKVStore, features, fallback, log.New("test.logger")) -} - // In memory kv store used for testing type FakeSecretsKVStore struct { store map[Key]string @@ -127,14 +104,6 @@ func buildKey(orgId int64, namespace string, typ string) Key { } } -func internalToProtoKey(k Key) *secretsmanagerplugin.Key { - return &secretsmanagerplugin.Key{ - OrgId: k.OrgId, - Namespace: k.Namespace, - Type: k.Type, - } -} - // Fake feature toggle - only need to check the backwards compatibility disabled flag type fakeFeatureToggles struct { returnValue bool @@ -158,172 +127,3 @@ func (f fakeFeatureToggles) IsEnabled(ctx context.Context, feature string) bool func (f fakeFeatureToggles) GetEnabled(ctx context.Context) map[string]bool { return map[string]bool{} } - -// Fake grpc secrets plugin impl -type fakeGRPCSecretsPlugin struct { - kv map[Key]string -} - -func (c *fakeGRPCSecretsPlugin) GetSecret(ctx context.Context, in *secretsmanagerplugin.GetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetSecretResponse, error) { - val, ok := c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)] - return &secretsmanagerplugin.GetSecretResponse{ - DecryptedValue: val, - Exists: ok, - }, nil -} - -func (c *fakeGRPCSecretsPlugin) SetSecret(ctx context.Context, in *secretsmanagerplugin.SetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.SetSecretResponse, error) { - c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)] = in.Value - return &secretsmanagerplugin.SetSecretResponse{}, nil -} - -func (c *fakeGRPCSecretsPlugin) DeleteSecret(ctx context.Context, in *secretsmanagerplugin.DeleteSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.DeleteSecretResponse, error) { - delete(c.kv, buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)) - return &secretsmanagerplugin.DeleteSecretResponse{}, nil -} - -func (c *fakeGRPCSecretsPlugin) ListSecrets(ctx context.Context, in *secretsmanagerplugin.ListSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.ListSecretsResponse, error) { - res := make([]*secretsmanagerplugin.Key, 0) - for k := range c.kv { - if in.KeyDescriptor.OrgId == AllOrganizations && in.KeyDescriptor.Namespace == "" && in.KeyDescriptor.Type == "" { - res = append(res, internalToProtoKey(k)) - } else if k.OrgId == in.KeyDescriptor.OrgId && k.Namespace == in.KeyDescriptor.Namespace && k.Type == in.KeyDescriptor.Type { - res = append(res, internalToProtoKey(k)) - } - } - return &secretsmanagerplugin.ListSecretsResponse{ - Keys: res, - }, nil -} - -func (c *fakeGRPCSecretsPlugin) RenameSecret(ctx context.Context, in *secretsmanagerplugin.RenameSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.RenameSecretResponse, error) { - oldKey := buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type) - val := c.kv[oldKey] - delete(c.kv, oldKey) - c.kv[buildKey(in.KeyDescriptor.OrgId, in.NewNamespace, in.KeyDescriptor.Type)] = val - return &secretsmanagerplugin.RenameSecretResponse{}, nil -} - -func (c *fakeGRPCSecretsPlugin) GetAllSecrets(ctx context.Context, in *secretsmanagerplugin.GetAllSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetAllSecretsResponse, error) { - items := make([]*secretsmanagerplugin.Item, 0) - for k, v := range c.kv { - items = append(items, &secretsmanagerplugin.Item{ - Key: internalToProtoKey(k), - Value: v, - }) - } - return &secretsmanagerplugin.GetAllSecretsResponse{ - Items: items, - }, nil -} - -var _ SecretsKVStore = &FakeSecretsKVStore{} -var _ secretsmanagerplugin.SecretsManagerPlugin = &fakeGRPCSecretsPlugin{} - -// Fake plugin manager -type fakePluginManager struct { - shouldFailOnStart bool - plugin *plugins.Plugin -} - -func (mg *fakePluginManager) SecretsManager(_ context.Context) *plugins.Plugin { - if mg.plugin != nil { - return mg.plugin - } - p := &plugins.Plugin{ - SecretsManager: &fakeGRPCSecretsPlugin{ - kv: make(map[Key]string), - }, - } - p.RegisterClient(&fakePluginClient{ - shouldFailOnStart: mg.shouldFailOnStart, - }) - mg.plugin = p - return p -} - -func NewFakeSecretsPluginManager(t *testing.T, shouldFailOnStart bool) plugins.SecretsPluginManager { - t.Helper() - return &fakePluginManager{ - shouldFailOnStart: shouldFailOnStart, - } -} - -// Fake plugin client -type fakePluginClient struct { - shouldFailOnStart bool - backendplugin.Plugin -} - -func (pc *fakePluginClient) Start(_ context.Context) error { - if pc.shouldFailOnStart { - return errors.New("mocked failed to start") - } - return nil -} - -func (pc *fakePluginClient) Stop(_ context.Context) error { - return nil -} - -func (pc *fakePluginClient) Logger() pluginsLogger.Logger { - return pluginsLogger.NewTestLogger() -} - -func SetupFatalCrashTest( - t *testing.T, - shouldFailOnStart bool, - isPluginErrorFatal bool, - isBackwardsCompatDisabled bool, -) (fatalCrashTestFields, error) { - t.Helper() - fatalFlagOnce = sync.Once{} - startupOnce = sync.Once{} - cfg := SetupTestConfig(t) - sqlStore := db.InitTestDB(t) - secretService := fakes.FakeSecretsService{} - kvstore := kvstore.ProvideService(sqlStore) - if isPluginErrorFatal { - _ = SetPluginStartupErrorFatal(context.Background(), GetNamespacedKVStore(kvstore), true) - } - features := NewFakeFeatureToggles(t, isBackwardsCompatDisabled) - manager := NewFakeSecretsPluginManager(t, shouldFailOnStart) - svc, err := ProvideService(sqlStore, secretService, manager, kvstore, features, cfg) - t.Cleanup(ResetPlugin) - return fatalCrashTestFields{ - SecretsKVStore: svc, - PluginManager: manager, - KVStore: kvstore, - SqlStore: sqlStore, - }, err -} - -type fatalCrashTestFields struct { - SecretsKVStore SecretsKVStore - PluginManager plugins.SecretsPluginManager - KVStore kvstore.KVStore - SqlStore db.DB -} - -func SetupTestConfig(t *testing.T) *setting.Cfg { - t.Helper() - rawCfg := ` - [secrets] - use_plugin = true - ` - raw, err := ini.Load([]byte(rawCfg)) - require.NoError(t, err) - return &setting.Cfg{Raw: raw} -} - -func ReplaceFallback(t *testing.T, kv SecretsKVStore, fb SecretsKVStore) error { - t.Helper() - if store, ok := kv.(*CachedKVStore); ok { - kv = store.store - } - if store, ok := kv.(*SecretsKVStorePlugin); ok { - store.fallbackStore = fb - return nil - } - return errors.New("not a plugin store") -} diff --git a/pkg/services/secrets/migrator/migrator.go b/pkg/services/secrets/migrator/migrator.go index 3581c2afd99..e4324a14104 100644 --- a/pkg/services/secrets/migrator/migrator.go +++ b/pkg/services/secrets/migrator/migrator.go @@ -51,6 +51,7 @@ func ProvideSecretsMigrator( b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "refresh_token"}, encoding: base64.StdEncoding}, b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "session_id"}, encoding: base64.StdEncoding}, b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "name_id"}, encoding: base64.StdEncoding}, + provisioningSecrets{}, } return &SecretsMigrator{ diff --git a/pkg/services/secrets/migrator/provisioning.go b/pkg/services/secrets/migrator/provisioning.go new file mode 100644 index 00000000000..8e4e6c3a669 --- /dev/null +++ b/pkg/services/secrets/migrator/provisioning.go @@ -0,0 +1,187 @@ +package migrator + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets/manager" +) + +var errNoEncryptedValue = errors.New("no encrypted value was found") + +var _ SecretsRotator = provisioningSecrets{} + +type provisioningSecrets struct{} + +func (p provisioningSecrets) ReEncrypt(ctx context.Context, svc *manager.SecretsService, sqlStore db.DB) bool { + return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) { + return svc.Encrypt(ctx, payload, secrets.WithoutScope()) + }, sqlStore, "rotation") +} + +func (p provisioningSecrets) Rollback(ctx context.Context, svc *manager.SecretsService, internal encryption.Internal, sqlStore db.DB, secretKey string) bool { + return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) { + return internal.Encrypt(ctx, payload, secretKey) + }, sqlStore, "rollback") +} + +func (p provisioningSecrets) reEncrypt( + ctx context.Context, + svc *manager.SecretsService, + encrypt func(context.Context, []byte) ([]byte, error), + sqlStore db.DB, + action string, +) bool { + var rows []struct { + Guid string + Value []byte + } + + if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + return sess.Table("resource").Where("`group` = 'provisioning.grafana.app' AND `resource` = 'repositories'"). + Select("guid, value"). + Find(&rows) + }); err != nil { + logger.Warn("Could not find any provisioning secrets to re-encrypt", "error", err, "action", action) + return false + } + + var failures int + + for _, row := range rows { + var resource map[string]any + if err := json.Unmarshal(row.Value, &resource); err != nil { + logger.Error("Failed to decode resource", "guid", row.Guid, "error", err, "action", action) + failures++ + continue + } + + err := p.reEncryptGitHubToken(ctx, svc, encrypt, sqlStore, resource) + if err != nil && !errors.Is(err, errNoEncryptedValue) { + logger.Error("Failed to rotate GitHub token", "guid", row.Guid, "error", err, "action", action) + failures++ + } + update := err == nil + + err = p.reEncryptWebhookSecret(ctx, svc, encrypt, sqlStore, resource) + if err != nil && !errors.Is(err, errNoEncryptedValue) { + logger.Error("Failed to rotate webhook secret", "guid", row.Guid, "error", err, "action", action) + failures++ + } + update = update || err == nil + + if update { + // Do it... + encoded, err := json.Marshal(resource) + if err != nil { + logger.Error("Failed to marshal resource to JSON", "guid", row.Guid, "error", err, "action", action) + failures++ + continue + } + + if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + // TODO: Figure out what resource_version is supposed to contain, and update it. + _, err = sess.Exec("UPDATE resource SET value = ? WHERE guid = ?", string(encoded), row.Guid) + return err + }); err != nil { + logger.Error("Failed to update resource with re-encrypted values", "guid", row.Guid, "error", err, "action", action) + failures++ + } + } + } + + if failures > 0 { + logger.Warn("Failed to rotate provisioning secrets", "failures", failures, "action", action) + } else { + logger.Info("Successfully rotated provisioning secrets", "action", action) + } + return failures > 0 +} + +func (provisioningSecrets) reEncryptGitHubToken( + ctx context.Context, + svc *manager.SecretsService, + encrypt func(context.Context, []byte) ([]byte, error), + sqlStore db.DB, + obj map[string]any, +) error { + spec, _ := getCast[map[string]any](obj, "spec") + github, _ := getCast[map[string]any](spec, "github") + b64Token, ok := getCast[string](github, "encryptedToken") + if !ok { + return errNoEncryptedValue + } + + token, err := base64.StdEncoding.DecodeString(b64Token) + if err != nil { + return fmt.Errorf("failed to decode base64 encrypted token: %w", err) + } + + decrypted, err := svc.Decrypt(ctx, token) + if err != nil { + return fmt.Errorf("failed to decrypt existing encrypted token: %w", err) + } + + encrypted, err := encrypt(ctx, decrypted) + if err != nil { + return fmt.Errorf("failed to encrypt decrypted token: %w", err) + } + + b64Token = base64.StdEncoding.EncodeToString(encrypted) + github["encryptedToken"] = b64Token + return nil +} + +func (provisioningSecrets) reEncryptWebhookSecret( + ctx context.Context, + svc *manager.SecretsService, + encrypt func(context.Context, []byte) ([]byte, error), + sqlStore db.DB, + obj map[string]any, +) error { + status, _ := getCast[map[string]any](obj, "status") + webhook, _ := getCast[map[string]any](status, "webhook") + b64Token, ok := getCast[string](webhook, "encryptedSecret") + if !ok { + return errNoEncryptedValue + } + + token, err := base64.StdEncoding.DecodeString(b64Token) + if err != nil { + return fmt.Errorf("failed to decode base64 encrypted secret: %w", err) + } + + decrypted, err := svc.Decrypt(ctx, token) + if err != nil { + return fmt.Errorf("failed to decrypt existing encrypted secret: %w", err) + } + + encrypted, err := encrypt(ctx, decrypted) + if err != nil { + return fmt.Errorf("failed to encrypt decrypted secret: %w", err) + } + + b64Token = base64.StdEncoding.EncodeToString(encrypted) + webhook["encryptedSecret"] = b64Token + return nil +} + +func getCast[T any](m map[string]any, name string) (T, bool) { + if m == nil { + var zero T + return zero, false + } + anyV, ok := m[name] + if ok { + t, ok := anyV.(T) + return t, ok + } + var zero T + return zero, false +} diff --git a/pkg/services/serviceaccounts/tests/fakes.go b/pkg/services/serviceaccounts/tests/fakes.go index 6e2a4427338..294069398d9 100644 --- a/pkg/services/serviceaccounts/tests/fakes.go +++ b/pkg/services/serviceaccounts/tests/fakes.go @@ -2,7 +2,6 @@ package tests import ( "context" - "fmt" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -58,7 +57,6 @@ func (f *FakeServiceAccountService) MigrateApiKey(ctx context.Context, orgID, ke } func (f *FakeServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { - fmt.Printf("fake migration result: %v", f.ExpectedMigrationResult) return f.ExpectedMigrationResult, f.ExpectedErr } diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 002b1e2e8a7..c6943ffb07d 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -261,6 +261,11 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add apiVersion for dashboard", NewAddColumnMigration(dashboardV2, &Column{ Name: "api_version", Type: DB_Varchar, Length: 16, Nullable: true, })) + + mg.AddMigration("Add index for dashboard_uid on dashboard_tag table", NewAddIndexMigration(dashboardTagV1, &Index{ + Cols: []string{"dashboard_uid"}, + Type: IndexType, + })) } type FillDashbordUIDAndOrgIDMigration struct { @@ -278,23 +283,23 @@ func (m *FillDashbordUIDAndOrgIDMigration) Exec(sess *xorm.Session, mg *Migrator func RunDashboardTagMigrations(sess *xorm.Session, driverName string) error { // sqlite sql := `UPDATE dashboard_tag - SET + SET dashboard_uid = (SELECT uid FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id), org_id = (SELECT org_id FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id) - WHERE + WHERE (dashboard_uid IS NULL OR org_id IS NULL) AND EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id);` if driverName == Postgres { - sql = `UPDATE dashboard_tag - SET dashboard_uid = dashboard.uid, + sql = `UPDATE dashboard_tag + SET dashboard_uid = dashboard.uid, org_id = dashboard.org_id - FROM dashboard + FROM dashboard WHERE dashboard_tag.dashboard_id = dashboard.id AND (dashboard_tag.dashboard_uid IS NULL OR dashboard_tag.org_id IS NULL);` } else if driverName == MySQL { - sql = `UPDATE dashboard_tag - LEFT JOIN dashboard ON dashboard_tag.dashboard_id = dashboard.id - SET dashboard_tag.dashboard_uid = dashboard.uid, + sql = `UPDATE dashboard_tag + LEFT JOIN dashboard ON dashboard_tag.dashboard_id = dashboard.id + SET dashboard_tag.dashboard_uid = dashboard.uid, dashboard_tag.org_id = dashboard.org_id WHERE dashboard_tag.dashboard_uid IS NULL OR dashboard_tag.org_id IS NULL;` } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 2b7be2663d6..bc95e01dc16 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -152,7 +152,7 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.AddAlertRuleMissingSeriesEvalsToResolve(mg) - ualert.AddAlertRuleVersionUIDIndex(mg) - accesscontrol.AddDatasourceDrilldownRemovalMigration(mg) + + ualert.DropTitleUniqueIndexMigration(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go index 3713e4eaa0a..ae1c1945e36 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go @@ -2,6 +2,8 @@ package ualert import ( "fmt" + "os" + "strconv" "strings" "github.com/google/uuid" @@ -29,10 +31,13 @@ func AddAlertRuleGuidMigration(mg *migrator.Migrator) { Nullable: false, Default: "''", })) - mg.AddMigration("drop index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewDropIndexMigration(alertRuleVersion, alertRuleVersionUDX_OrgIdRuleUIDVersion)) + + mg.AddMigration("cleanup alert_rule_version table", &cleanUpRuleVersionsMigration{}) mg.AddMigration("populate rule guid in alert rule table", &setRuleGuidMigration{}) + mg.AddMigration("drop index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewDropIndexMigration(alertRuleVersion, alertRuleVersionUDX_OrgIdRuleUIDVersion)) + mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns", migrator.NewAddIndexMigration(alertRuleVersion, &migrator.Index{Cols: []string{"rule_org_id", "rule_uid", "rule_guid", "version"}, Type: migrator.UniqueIndex}, @@ -118,3 +123,80 @@ func (c setRuleGuidMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) er } return nil } + +type cleanUpRuleVersionsMigration struct { + migrator.MigrationBase +} + +var _ migrator.CodeMigration = (*cleanUpRuleVersionsMigration)(nil) + +func (c cleanUpRuleVersionsMigration) SQL(migrator.Dialect) string { + return codeMigration +} + +func getBatchSize() int { + const defaultBatchSize = 50 + envvar := os.Getenv("ALERT_RULE_VERSION_CLEANUP_MIGRATION_BATCH_SIZE") + if envvar == "" { + return defaultBatchSize + } + batchSize, err := strconv.Atoi(envvar) + if err != nil { + return defaultBatchSize + } + return batchSize +} + +func (c cleanUpRuleVersionsMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + var batchSize = getBatchSize() + + const maxRetention = 100 + toKeep := mg.Cfg.UnifiedAlerting.RuleVersionRecordLimit + if toKeep <= 0 { + mg.Logger.Info("Rule version record limit is not set, fallback to 100", "limit", toKeep) + toKeep = maxRetention + } + + var rules []alertRule + err := sess.Table(alertRule{}).Select("uid, version").Where("version > ?", toKeep).Find(&rules) + if err != nil { + return err + } + mg.Logger.Debug("Got alert rule UIDs with versions greater than retention", "count", len(rules)) + batches := len(rules) / batchSize + if len(rules)%batchSize != 0 { + batches++ + } + + mg.Logger.Info("Cleaning up table `alert_rule_version`", "batchSize", batchSize, "batches", batches, "keepVersions", toKeep) + + for i := 0; i < batches; i++ { + end := i*batchSize + batchSize + if end > len(rules) { + end = len(rules) + } + bd := strings.Builder{} + for idx, r := range rules[i*batchSize : end] { + if idx == 0 { + bd.WriteString(fmt.Sprintf("SELECT '%s' as uid, %d as version", r.UID, r.Version)) + continue + } + bd.WriteString(fmt.Sprintf(" UNION ALL SELECT '%s', %d ", r.UID, r.Version)) + } + _, err = sess.Exec(fmt.Sprintf(` + DELETE FROM alert_rule_version + WHERE EXISTS ( + SELECT 1 + FROM (%s) AR + WHERE AR.uid = alert_rule_version.rule_uid + AND alert_rule_version.version < AR.version - %d + )`, bd.String(), toKeep), + ) + if err != nil { + return err + } + + mg.Logger.Debug(fmt.Sprintf("Batch %d of %d processed", i+1, batches)) + } + return nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_version_uid_index.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_version_uid_index.go deleted file mode 100644 index 7d24bc8086c..00000000000 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_version_uid_index.go +++ /dev/null @@ -1,15 +0,0 @@ -package ualert - -import ( - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" -) - -// AddAlertRuleVersionUIDIndex adds an index to the alert_rule_version table on (rule_org_id, rule_uid) columns. -func AddAlertRuleVersionUIDIndex(mg *migrator.Migrator) { - mg.AddMigration("add index to alert_rule_version table on (rule_org_id, rule_uid)", - migrator.NewAddIndexMigration( - migrator.Table{Name: "alert_rule_version"}, - &migrator.Index{Cols: []string{"rule_org_id", "rule_uid"}, Type: migrator.IndexType}, - ), - ) -} diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index 023deb2a3cb..4990939c670 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -195,6 +195,8 @@ func alertInstanceMigration(mg *migrator.Migrator) { })) } +var titleUniqueIndex = &migrator.Index{Cols: []string{"org_id", "namespace_uid", "title"}, Type: migrator.UniqueIndex} + func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { // DO NOT EDIT alertRule := migrator.Table{ @@ -245,9 +247,7 @@ func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) Cols: []string{"org_id", "title"}, Type: migrator.UniqueIndex, })) - mg.AddMigration("add index in alert_rule on org_id, namespase_uid and title columns", migrator.NewAddIndexMigration(alertRule, &migrator.Index{ - Cols: []string{"org_id", "namespace_uid", "title"}, Type: migrator.UniqueIndex, - })) + mg.AddMigration("add index in alert_rule on org_id, namespase_uid and title columns", migrator.NewAddIndexMigration(alertRule, titleUniqueIndex)) mg.AddMigration("add dashboard_uid column to alert_rule", migrator.NewAddColumnMigration( migrator.Table{Name: "alert_rule"}, @@ -571,3 +571,8 @@ func (c extractAlertmanagerConfigurationHistory) Exec(sess *xorm.Session, migrat } return nil } + +func DropTitleUniqueIndexMigration(mg *migrator.Migrator) { + mg.AddMigration("remove title in folder unique index", + migrator.NewDropIndexMigration(migrator.Table{Name: "alert_rule"}, titleUniqueIndex)) +} diff --git a/pkg/services/sqlstore/migrations/user_auth_mig.go b/pkg/services/sqlstore/migrations/user_auth_mig.go index bf3b98955b7..9546d84a766 100644 --- a/pkg/services/sqlstore/migrations/user_auth_mig.go +++ b/pkg/services/sqlstore/migrations/user_auth_mig.go @@ -46,4 +46,8 @@ func addUserAuthMigrations(mg *Migrator) { mg.AddMigration("Add OAuth ID token to user_auth", NewAddColumnMigration(userAuthV1, &Column{ Name: "o_auth_id_token", Type: DB_Text, Nullable: true, })) + + mg.AddMigration("Add user_unique_id to user_auth", NewAddColumnMigration(userAuthV1, &Column{ + Name: "external_uid", Type: DB_Text, Nullable: true, + })) } diff --git a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go index b1ad97f621e..bb2c878e296 100644 --- a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go +++ b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go @@ -72,6 +72,20 @@ func (p *ServiceAccountsSameLoginCrossOrgs) Exec(sess *xorm.Session, mg *migrato AND is_service_account = 1 AND login NOT LIKE 'sa-' || CAST(org_id AS TEXT) || '-%'; `) + case migrator.Spanner: + _, err = p.sess.Exec(` + UPDATE user + SET login = CONCAT('sa-', CAST(org_id AS STRING), '-', + CASE + WHEN login LIKE 'sa-%' THEN SUBSTRING(login, 4) + ELSE login + END + ) + WHERE login IS NOT NULL + AND is_service_account + AND login NOT LIKE CONCAT('sa-', CAST(org_id AS STRING), '-%') + `) + default: return fmt.Errorf("dialect not supported: %s", p.dialect) } @@ -128,6 +142,19 @@ func (p *ServiceAccountsDeduplicateOrgInLogin) Exec(sess *xorm.Session, mg *migr WHERE u2.login = 'sa-' || CAST(u.org_id AS TEXT) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS TEXT)||'-'||CAST(u.org_id AS TEXT))+1) );; `) + case migrator.Spanner: + _, err = sess.Exec(` + UPDATE ` + dialect.Quote("user") + ` AS u + SET login = 'sa-' || CAST(u.org_id AS STRING) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING))+1) + WHERE u.login IS NOT NULL + AND u.is_service_account + AND u.login LIKE 'sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING)||'-%' + AND NOT EXISTS ( + SELECT 1 + FROM ` + dialect.Quote("user") + `AS u2 + WHERE u2.login = 'sa-' || CAST(u.org_id AS STRING) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING))+1) + );; + `) default: return fmt.Errorf("dialect not supported: %s", dialect) } diff --git a/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go b/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go index 570b79ba52e..2d85a64a426 100644 --- a/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go +++ b/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go @@ -30,7 +30,7 @@ func (p *UsersLowerCaseLoginAndEmail) SQL(dialect migrator.Dialect) string { func (p *UsersLowerCaseLoginAndEmail) Exec(sess *xorm.Session, mg *migrator.Migrator) error { // Get all users users := make([]*user.User, 0) - err := sess.Table("user").Find(&users) + err := sess.Table("user").Asc("created").Find(&users) if err != nil { return err } diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 4a3c51af7ec..55a10768a80 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -171,7 +171,7 @@ func (db *MySQLDialect) CleanDB(engine *xorm.Engine) error { // TruncateDBTables truncates all the tables. // A special case is the dashboard_acl table where we keep the default permissions. func (db *MySQLDialect) TruncateDBTables(engine *xorm.Engine) error { - tables, err := engine.DBMetas() + tables, err := engine.Dialect().GetTables() if err != nil { return err } diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 73c02adf64e..e2401d980bc 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -141,7 +141,7 @@ func (db *PostgresDialect) CleanDB(engine *xorm.Engine) error { // TruncateDBTables truncates all the tables. // A special case is the dashboard_acl table where we keep the default permissions. func (db *PostgresDialect) TruncateDBTables(engine *xorm.Engine) error { - tables, err := engine.DBMetas() + tables, err := engine.Dialect().GetTables() if err != nil { return err } diff --git a/pkg/services/sqlstore/migrator/snapshot/spanner-ddl.json b/pkg/services/sqlstore/migrator/snapshot/spanner-ddl.json index a205475caa6..3fe36f0b903 100644 --- a/pkg/services/sqlstore/migrator/snapshot/spanner-ddl.json +++ b/pkg/services/sqlstore/migrator/snapshot/spanner-ddl.json @@ -16,7 +16,7 @@ "CREATE TABLE `alert_notification_state` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `alert_id` INT64 NOT NULL, `notifier_id` INT64 NOT NULL, `state` STRING(50) NOT NULL, `version` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `alert_rule_state_updated_version` INT64 NOT NULL) PRIMARY KEY (id)", "CREATE INDEX `IDX_alert_notification_state_alert_id` ON `alert_notification_state` (alert_id)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_notification_state_org_id_alert_id_notifier_id` ON `alert_notification_state` (org_id, alert_id, notifier_id)", - "CREATE TABLE `alert_rule` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `updated` TIMESTAMP NOT NULL, `interval_seconds` INT64 NOT NULL DEFAULT (60), `version` INT64 NOT NULL DEFAULT (0), `uid` STRING(40) NOT NULL DEFAULT ('0'), `namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `dashboard_uid` STRING(40), `panel_id` INT64, `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `updated_by` STRING(40), `guid` STRING(36) NOT NULL DEFAULT ('')) PRIMARY KEY (id)", + "CREATE TABLE `alert_rule` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `updated` TIMESTAMP NOT NULL, `interval_seconds` INT64 NOT NULL DEFAULT (60), `version` INT64 NOT NULL DEFAULT (0), `uid` STRING(40) NOT NULL DEFAULT ('0'), `namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `dashboard_uid` STRING(40), `panel_id` INT64, `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `updated_by` STRING(40), `guid` STRING(36) NOT NULL DEFAULT (''), `missing_series_evals_to_resolve` INT64) PRIMARY KEY (id)", "CREATE INDEX `IDX_alert_rule_org_id_dashboard_uid_panel_id` ON `alert_rule` (org_id, dashboard_uid, panel_id)", "CREATE INDEX `IDX_alert_rule_org_id_namespace_uid_rule_group` ON `alert_rule` (org_id, namespace_uid, rule_group)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_guid` ON `alert_rule` (guid)", @@ -27,7 +27,7 @@ "CREATE TABLE `alert_rule_tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `alert_id` INT64 NOT NULL, `tag_id` INT64 NOT NULL) PRIMARY KEY (id)", "CREATE INDEX `IDX_alert_rule_tag_alert_id` ON `alert_rule_tag` (alert_id)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_tag_alert_id_tag_id` ON `alert_rule_tag` (alert_id, tag_id)", - "CREATE TABLE `alert_rule_version` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `rule_org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL DEFAULT ('0'), `rule_namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `parent_version` INT64 NOT NULL, `restored_from` INT64 NOT NULL, `version` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `interval_seconds` INT64 NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `created_by` STRING(40), `rule_guid` STRING(36) NOT NULL DEFAULT ('')) PRIMARY KEY (id)", + "CREATE TABLE `alert_rule_version` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `rule_org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL DEFAULT ('0'), `rule_namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `parent_version` INT64 NOT NULL, `restored_from` INT64 NOT NULL, `version` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `interval_seconds` INT64 NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `created_by` STRING(40), `rule_guid` STRING(36) NOT NULL DEFAULT (''), `missing_series_evals_to_resolve` INT64) PRIMARY KEY (id)", "CREATE INDEX `IDX_alert_rule_version_rule_org_id_rule_namespace_uid_rule_group` ON `alert_rule_version` (rule_org_id, rule_namespace_uid, rule_group)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_version_rule_guid_version` ON `alert_rule_version` (rule_guid, version)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_version_rule_org_id_rule_uid_rule_guid_version` ON `alert_rule_version` (rule_org_id, rule_uid, rule_guid, version)", @@ -253,5 +253,9 @@ "CREATE INDEX `IDX_user_role_user_id` ON `user_role` (user_id)", "CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` ON `user_role` (org_id, user_id, role_id, group_mapping_uid)", "CREATE TABLE `user_stats` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `billing_role` STRING(40) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)", - "CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_stats_user_id` ON `user_stats` (user_id)" + "CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_stats_user_id` ON `user_stats` (user_id)", + "CREATE TABLE resource ( namespace STRING(63), resource_group STRING(190), resource STRING(190), name STRING(253), folder STRING(253), value BYTES(MAX), resource_version TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ), previous_resource_version TIMESTAMP, ) PRIMARY KEY (namespace, resource_group, resource, name)", + "CREATE TABLE resource_history ( namespace STRING(63), resource_group STRING(190), resource STRING(190), name STRING(253), folder STRING(253), value BYTES(MAX), resource_version TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ), previous_resource_version TIMESTAMP, action INT64, ) PRIMARY KEY (namespace, resource_group, resource, name, resource_version DESC)", + "CREATE TABLE resource_blob ( uid STRING(36) NOT NULL, resource_key STRING(MAX) NOT NULL, content_type STRING(100), value BYTES(MAX), ) PRIMARY KEY (uid)", + "CREATE CHANGE STREAM resource_stream FOR resource" ] diff --git a/pkg/services/sqlstore/migrator/snapshot/spanner-log.json b/pkg/services/sqlstore/migrator/snapshot/spanner-log.json index f9f39d4083b..e1944599ca3 100644 --- a/pkg/services/sqlstore/migrator/snapshot/spanner-log.json +++ b/pkg/services/sqlstore/migrator/snapshot/spanner-log.json @@ -642,6 +642,8 @@ "add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns", "add index in alert_rule_version table on rule_guid and version columns", "add index in alert_rule table on guid columns", + "add missing_series_evals_to_resolve column to alert_rule", + "add missing_series_evals_to_resolve column to alert_rule_version", "create data_source_usage_by_day table", "create data_source_usage_by_day(data_source_id) index", "create data_source_usage_by_day(data_source_id, day) unique index", diff --git a/pkg/services/sqlstore/migrator/spanner_dialect.go b/pkg/services/sqlstore/migrator/spanner_dialect.go index 31a6fbb354d..1bd540ea4b1 100644 --- a/pkg/services/sqlstore/migrator/spanner_dialect.go +++ b/pkg/services/sqlstore/migrator/spanner_dialect.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "strconv" "time" "cloud.google.com/go/spanner" @@ -15,10 +14,7 @@ import ( "github.com/googleapis/gax-go/v2" spannerdriver "github.com/googleapis/go-sql-spanner" "github.com/grafana/dskit/concurrency" - "google.golang.org/api/option" - "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" "xorm.io/core" "xorm.io/xorm" @@ -147,6 +143,9 @@ func (s *SpannerDialect) TruncateDBTables(engine *xorm.Engine) error { switch table.Name { case "": continue + case "autoincrement_sequences": + // Don't delete sequence number for migration_log.id column. + statements = append(statements, fmt.Sprintf("DELETE FROM %v WHERE name <> 'migration_log:id'", s.Quote(table.Name))) case "migration_log": continue case "dashboard_acl": @@ -173,12 +172,15 @@ func (s *SpannerDialect) CleanDB(engine *xorm.Engine) error { // Collect all DROP statements. var statements []string - for _, table := range tables { - // Ignore these tables used by Unified storage. - if table.Name == "resource" || table.Name == "resource_blob" || table.Name == "resource_history" { - continue - } + changeStreams, err := s.findChangeStreams(engine) + if err != nil { + return err + } + for _, cs := range changeStreams { + statements = append(statements, fmt.Sprintf("DROP CHANGE STREAM `%s`", cs)) + } + for _, table := range tables { // Indexes must be dropped first, otherwise dropping tables fails. for _, index := range table.Indexes { if !index.IsRegular { @@ -288,7 +290,7 @@ func (s *SpannerDialect) executeDDLStatements(ctx context.Context, engine *xorm. return err } - opts := SpannerConnectorConfigToClientOptions(cfg) + opts := xorm.SpannerConnectorConfigToClientOptions(cfg) databaseAdminClient, err := database.NewDatabaseAdminClient(ctx, opts...) if err != nil { @@ -313,28 +315,27 @@ func (s *SpannerDialect) executeDDLStatements(ctx context.Context, engine *xorm. return nil } -// SpannerConnectorConfigToClientOptions is adapted from https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L341-L477, from version 1.11.1. -func SpannerConnectorConfigToClientOptions(connectorConfig spannerdriver.ConnectorConfig) []option.ClientOption { - var opts []option.ClientOption - if connectorConfig.Host != "" { - opts = append(opts, option.WithEndpoint(connectorConfig.Host)) - } - if strval, ok := connectorConfig.Params["credentials"]; ok { - opts = append(opts, option.WithCredentialsFile(strval)) - } - if strval, ok := connectorConfig.Params["credentialsjson"]; ok { - opts = append(opts, option.WithCredentialsJSON([]byte(strval))) - } - if strval, ok := connectorConfig.Params["useplaintext"]; ok { - if val, err := strconv.ParseBool(strval); err == nil && val { - opts = append(opts, - option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), - option.WithoutAuthentication()) - } - } - return opts -} - func (s *SpannerDialect) UnionDistinct() string { return "UNION DISTINCT" } + +func (s *SpannerDialect) findChangeStreams(engine *xorm.Engine) ([]string, error) { + var result []string + query := `SELECT c.CHANGE_STREAM_NAME + FROM INFORMATION_SCHEMA.CHANGE_STREAMS AS C + WHERE C.CHANGE_STREAM_CATALOG='' + AND C.CHANGE_STREAM_SCHEMA=''` + rows, err := engine.DB().Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + result = append(result, name) + } + return result, nil +} diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index 7a691b4d5d2..8303527ba40 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -103,7 +103,7 @@ func (db *SQLite3) CleanDB(engine *xorm.Engine) error { // TruncateDBTables deletes all data from all the tables and resets the sequences. // A special case is the dashboard_acl table where we keep the default permissions. func (db *SQLite3) TruncateDBTables(engine *xorm.Engine) error { - tables, err := engine.DBMetas() + tables, err := engine.Dialect().GetTables() if err != nil { return err } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 2f625298937..f26e923228f 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -760,9 +760,21 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access }) } - _, err := sess.InsertMulti(&dashes) - if err != nil { - return err + // Insert dashboards in batches + batchSize := 500 + if db.IsTestDBSpanner() { + batchSize = 30 // spanner has a limit of 950 parameters per query + } + for i := 0; i < len(dashes); i += batchSize { + end := i + batchSize + if end > len(dashes) { + end = len(dashes) + } + + _, err := sess.InsertMulti(dashes[i:end]) + if err != nil { + return err + } } role := &accesscontrol.Role{ @@ -772,7 +784,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access Updated: time.Now(), Created: time.Now(), } - _, err = sess.Insert(role) + _, err := sess.Insert(role) if err != nil { return err } @@ -794,10 +806,18 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access permissions[i].Updated = time.Now() permissions[i].Kind, permissions[i].Attribute, permissions[i].Identifier = permissions[i].SplitScope() } + if len(permissions) > 0 { - _, err = sess.InsertMulti(&permissions) - if err != nil { - return err + for i := 0; i < len(permissions); i += batchSize { + end := i + batchSize + if end > len(permissions) { + end = len(permissions) + } + + _, err = sess.InsertMulti(permissions[i:end]) + if err != nil { + return err + } } } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index a3029fffe77..10dcf1d0c14 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -628,6 +628,8 @@ func TestMain(m *testing.M) { if err := testSQLStore.dialect.TruncateDBTables(testSQLStore.GetEngine()); err != nil { return nil, err } + testSQLStore.engine.ResetSequenceGenerator() + if err := testSQLStore.Reset(); err != nil { return nil, err } diff --git a/pkg/services/sqlstore/sqlstore_testinfra.go b/pkg/services/sqlstore/sqlstore_testinfra.go index fa7e1c47b3d..0a5c4b5db4f 100644 --- a/pkg/services/sqlstore/sqlstore_testinfra.go +++ b/pkg/services/sqlstore/sqlstore_testinfra.go @@ -183,6 +183,7 @@ func NewTestStore(tb TestingTB, opts ...TestOption) *SQLStore { tb.Fatalf("failed to truncate DB tables after migrations: %v", err) panic("unreachable") } + testSQLStore.engine.ResetSequenceGenerator() } return store diff --git a/pkg/services/ssosettings/strategies/saml_strategy.go b/pkg/services/ssosettings/strategies/saml_strategy.go index 026fd5cef7f..a863b9f491e 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy.go +++ b/pkg/services/ssosettings/strategies/saml_strategy.go @@ -32,43 +32,44 @@ func (s *SAMLStrategy) GetProviderConfig(_ context.Context, provider string) (ma func (s *SAMLStrategy) loadSAMLSettings() map[string]any { section := s.settingsProvider.Section("auth.saml") result := map[string]any{ - "enabled": section.KeyValue("enabled").MustBool(false), - "entity_id": section.KeyValue("entity_id").MustString(""), - "name": section.KeyValue("name").MustString("SAML"), - "single_logout": section.KeyValue("single_logout").MustBool(false), - "allow_sign_up": section.KeyValue("allow_sign_up").MustBool(false), - "auto_login": section.KeyValue("auto_login").MustBool(false), - "certificate": section.KeyValue("certificate").MustString(""), - "certificate_path": section.KeyValue("certificate_path").MustString(""), - "private_key": section.KeyValue("private_key").MustString(""), - "private_key_path": section.KeyValue("private_key_path").MustString(""), - "signature_algorithm": section.KeyValue("signature_algorithm").MustString(""), - "idp_metadata": section.KeyValue("idp_metadata").MustString(""), - "idp_metadata_path": section.KeyValue("idp_metadata_path").MustString(""), - "idp_metadata_url": section.KeyValue("idp_metadata_url").MustString(""), - "max_issue_delay": section.KeyValue("max_issue_delay").MustDuration(90 * time.Second), - "metadata_valid_duration": section.KeyValue("metadata_valid_duration").MustDuration(48 * time.Hour), - "allow_idp_initiated": section.KeyValue("allow_idp_initiated").MustBool(false), - "relay_state": section.KeyValue("relay_state").MustString(""), - "assertion_attribute_name": section.KeyValue("assertion_attribute_name").MustString(""), - "assertion_attribute_login": section.KeyValue("assertion_attribute_login").MustString(""), - "assertion_attribute_email": section.KeyValue("assertion_attribute_email").MustString(""), - "assertion_attribute_groups": section.KeyValue("assertion_attribute_groups").MustString(""), - "assertion_attribute_role": section.KeyValue("assertion_attribute_role").MustString(""), - "assertion_attribute_org": section.KeyValue("assertion_attribute_org").MustString(""), - "allowed_organizations": section.KeyValue("allowed_organizations").MustString(""), - "org_mapping": section.KeyValue("org_mapping").MustString(""), - "role_values_none": section.KeyValue("role_values_none").MustString(""), - "role_values_viewer": section.KeyValue("role_values_viewer").MustString(""), - "role_values_editor": section.KeyValue("role_values_editor").MustString(""), - "role_values_admin": section.KeyValue("role_values_admin").MustString(""), - "role_values_grafana_admin": section.KeyValue("role_values_grafana_admin").MustString(""), - "name_id_format": section.KeyValue("name_id_format").MustString(""), - "skip_org_role_sync": section.KeyValue("skip_org_role_sync").MustBool(false), - "client_id": section.KeyValue("client_id").MustString(""), - "client_secret": section.KeyValue("client_secret").MustString(""), - "token_url": section.KeyValue("token_url").MustString(""), - "force_use_graph_api": section.KeyValue("force_use_graph_api").MustBool(false), + "allow_idp_initiated": section.KeyValue("allow_idp_initiated").MustBool(false), + "allow_sign_up": section.KeyValue("allow_sign_up").MustBool(false), + "allowed_organizations": section.KeyValue("allowed_organizations").MustString(""), + "assertion_attribute_email": section.KeyValue("assertion_attribute_email").MustString(""), + "assertion_attribute_groups": section.KeyValue("assertion_attribute_groups").MustString(""), + "assertion_attribute_login": section.KeyValue("assertion_attribute_login").MustString(""), + "assertion_attribute_name": section.KeyValue("assertion_attribute_name").MustString(""), + "assertion_attribute_org": section.KeyValue("assertion_attribute_org").MustString(""), + "assertion_attribute_role": section.KeyValue("assertion_attribute_role").MustString(""), + "auto_login": section.KeyValue("auto_login").MustBool(false), + "certificate": section.KeyValue("certificate").MustString(""), + "certificate_path": section.KeyValue("certificate_path").MustString(""), + "client_id": section.KeyValue("client_id").MustString(""), + "client_secret": section.KeyValue("client_secret").MustString(""), + "enabled": section.KeyValue("enabled").MustBool(false), + "entity_id": section.KeyValue("entity_id").MustString(""), + "external_uid_assertion_name": section.KeyValue("external_uid_assertion_name").MustString(""), + "force_use_graph_api": section.KeyValue("force_use_graph_api").MustBool(false), + "idp_metadata": section.KeyValue("idp_metadata").MustString(""), + "idp_metadata_path": section.KeyValue("idp_metadata_path").MustString(""), + "idp_metadata_url": section.KeyValue("idp_metadata_url").MustString(""), + "max_issue_delay": section.KeyValue("max_issue_delay").MustDuration(90 * time.Second), + "metadata_valid_duration": section.KeyValue("metadata_valid_duration").MustDuration(48 * time.Hour), + "name": section.KeyValue("name").MustString("SAML"), + "name_id_format": section.KeyValue("name_id_format").MustString(""), + "org_mapping": section.KeyValue("org_mapping").MustString(""), + "private_key": section.KeyValue("private_key").MustString(""), + "private_key_path": section.KeyValue("private_key_path").MustString(""), + "relay_state": section.KeyValue("relay_state").MustString(""), + "role_values_admin": section.KeyValue("role_values_admin").MustString(""), + "role_values_editor": section.KeyValue("role_values_editor").MustString(""), + "role_values_grafana_admin": section.KeyValue("role_values_grafana_admin").MustString(""), + "role_values_none": section.KeyValue("role_values_none").MustString(""), + "role_values_viewer": section.KeyValue("role_values_viewer").MustString(""), + "signature_algorithm": section.KeyValue("signature_algorithm").MustString(""), + "single_logout": section.KeyValue("single_logout").MustBool(false), + "skip_org_role_sync": section.KeyValue("skip_org_role_sync").MustBool(false), + "token_url": section.KeyValue("token_url").MustString(""), } return result } diff --git a/pkg/services/ssosettings/strategies/saml_strategy_test.go b/pkg/services/ssosettings/strategies/saml_strategy_test.go index c04ef280318..507005a45b2 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy_test.go +++ b/pkg/services/ssosettings/strategies/saml_strategy_test.go @@ -54,43 +54,44 @@ var ( ` expectedSAMLInfo = map[string]any{ - "enabled": true, - "entity_id": "custom-entity-id", - "single_logout": true, - "allow_sign_up": true, - "auto_login": true, - "name": "SAML Test", - "certificate": "devenv/docker/blocks/auth/saml-enterprise/cert.crt", - "certificate_path": "/path/to/cert", - "private_key": "dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0", - "private_key_path": "devenv/docker/blocks/auth/saml-enterprise/key.pem", - "signature_algorithm": "rsa-sha256", - "idp_metadata": "dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0", - "idp_metadata_path": "/path/to/metadata", - "idp_metadata_url": "http://localhost:8086/realms/grafana/protocol/saml/descriptor", - "max_issue_delay": 90 * time.Second, - "metadata_valid_duration": 48 * time.Hour, - "allow_idp_initiated": false, - "relay_state": "relay_state", - "assertion_attribute_name": "name", - "assertion_attribute_login": "login", - "assertion_attribute_email": "email", - "assertion_attribute_groups": "groups", - "assertion_attribute_role": "roles", - "assertion_attribute_org": "orgs", - "allowed_organizations": "org1 org2", - "org_mapping": "org1:1:editor, *:2:viewer", - "role_values_viewer": "viewer", - "role_values_editor": "editor", - "role_values_admin": "admin", - "role_values_grafana_admin": "serveradmin", - "name_id_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "skip_org_role_sync": false, - "role_values_none": "guest disabled", - "token_url": "http://localhost:8086/auth/realms/grafana/protocol/openid-connect/token", - "client_id": "grafana", - "client_secret": "grafana", - "force_use_graph_api": false, + "enabled": true, + "entity_id": "custom-entity-id", + "external_uid_assertion_name": "", + "single_logout": true, + "allow_sign_up": true, + "auto_login": true, + "name": "SAML Test", + "certificate": "devenv/docker/blocks/auth/saml-enterprise/cert.crt", + "certificate_path": "/path/to/cert", + "private_key": "dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0", + "private_key_path": "devenv/docker/blocks/auth/saml-enterprise/key.pem", + "signature_algorithm": "rsa-sha256", + "idp_metadata": "dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0", + "idp_metadata_path": "/path/to/metadata", + "idp_metadata_url": "http://localhost:8086/realms/grafana/protocol/saml/descriptor", + "max_issue_delay": 90 * time.Second, + "metadata_valid_duration": 48 * time.Hour, + "allow_idp_initiated": false, + "relay_state": "relay_state", + "assertion_attribute_name": "name", + "assertion_attribute_login": "login", + "assertion_attribute_email": "email", + "assertion_attribute_groups": "groups", + "assertion_attribute_role": "roles", + "assertion_attribute_org": "orgs", + "allowed_organizations": "org1 org2", + "org_mapping": "org1:1:editor, *:2:viewer", + "role_values_viewer": "viewer", + "role_values_editor": "editor", + "role_values_admin": "admin", + "role_values_grafana_admin": "serveradmin", + "name_id_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "skip_org_role_sync": false, + "role_values_none": "guest disabled", + "token_url": "http://localhost:8086/auth/realms/grafana/protocol/openid-connect/token", + "client_id": "grafana", + "client_secret": "grafana", + "force_use_graph_api": false, } ) diff --git a/pkg/services/store/testdata/public_testdata.golden.jsonc b/pkg/services/store/testdata/public_testdata.golden.jsonc index 03f9a7f16cc..ea77fd3d487 100644 --- a/pkg/services/store/testdata/public_testdata.golden.jsonc +++ b/pkg/services/store/testdata/public_testdata.golden.jsonc @@ -87,5 +87,4 @@ } } ] -} - +} \ No newline at end of file diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index 09a456d81d5..d0552d16d0f 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -20,6 +20,7 @@ type FakeUserService struct { UpdateFn func(ctx context.Context, cmd *user.UpdateUserCommand) error GetSignedInUserFn func(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) CreateFn func(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) + GetByLoginFn func(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) BatchDisableUsersFn func(ctx context.Context, cmd *user.BatchDisableUsersCommand) error GetByEmailFn func(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) @@ -59,6 +60,9 @@ func (f *FakeUserService) GetByUID(ctx context.Context, query *user.GetUserByUID } func (f *FakeUserService) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { + if f.GetByLoginFn != nil { + return f.GetByLoginFn(ctx, query) + } return f.ExpectedUser, f.ExpectedError } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 815553d3813..43d78edacef 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -129,12 +129,13 @@ type Cfg struct { Packaging string // Paths - HomePath string - ProvisioningPath string - DataPath string - LogsPath string - PluginsPath string - EnterpriseLicensePath string + HomePath string + ProvisioningPath string + PermittedProvisioningPaths []string + DataPath string + LogsPath string + PluginsPath string + EnterpriseLicensePath string // SMTP email settings Smtp SmtpSettings @@ -181,6 +182,9 @@ type Cfg struct { DataProxyWhiteList map[string]bool ActionsAllowPostURL string + // K8s Dashboard Cleanup + K8sDashboardCleanup K8sDashboardCleanupSettings + TempDataLifetime time.Duration // Plugins @@ -549,8 +553,6 @@ type Cfg struct { HttpsSkipVerify bool } -const UnifiedStorageConfigKeyDashboard = "dashboards.dashboard.grafana.app" - type UnifiedStorageConfig struct { DualWriterMode rest.DualWriterMode DualWriterPeriodicDataSyncJobEnabled bool @@ -1138,6 +1140,10 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { return err } + if err := cfg.readProvisioningSettings(iniFile); err != nil { + return err + } + // read dashboard settings dashboards := iniFile.Section("dashboards") cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) @@ -1291,6 +1297,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.readDataSourcesSettings() cfg.readDataSourceSecuritySettings() + cfg.readK8sDashboardCleanupSettings() cfg.readSqlDataSourceSettings() cfg.Storage = readStorageSettings(iniFile) @@ -2022,6 +2029,24 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { return nil } +func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error { + provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") + cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath) + + provisioningPaths := strings.TrimSpace(valueAsString(iniFile.Section("paths"), "permitted_provisioning_paths", "")) + if provisioningPaths != "|" && provisioningPaths != "" { + cfg.PermittedProvisioningPaths = strings.Split(provisioningPaths, "|") + for i, s := range cfg.PermittedProvisioningPaths { + s = strings.TrimSpace(s) + if s == "" { + return fmt.Errorf("a provisioning path is empty in '%s' (at index %d)", provisioningPaths, i) + } + cfg.PermittedProvisioningPaths[i] = makeAbsolute(s, cfg.HomePath) + } + } + return nil +} + func (cfg *Cfg) readPublicDashboardsSettings() { publicDashboards := cfg.Raw.Section("public_dashboards") cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true) diff --git a/pkg/setting/setting_cloud_migration.go b/pkg/setting/setting_cloud_migration.go index 57bacc1d20b..dce1ac5fa7c 100644 --- a/pkg/setting/setting_cloud_migration.go +++ b/pkg/setting/setting_cloud_migration.go @@ -32,7 +32,6 @@ type CloudMigrationSettings struct { CreateTokenTimeout time.Duration DeleteTokenTimeout time.Duration TokenExpiresAfter time.Duration - FeedbackURL string FrontendPollInterval time.Duration AlertRulesState string @@ -60,7 +59,6 @@ func (cfg *Cfg) readCloudMigrationSettings() { cfg.CloudMigration.DeleteTokenTimeout = cloudMigration.Key("delete_token_timeout").MustDuration(5 * time.Second) cfg.CloudMigration.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour) cfg.CloudMigration.IsDeveloperMode = cloudMigration.Key("developer_mode").MustBool(false) - cfg.CloudMigration.FeedbackURL = cloudMigration.Key("feedback_url").MustString("") cfg.CloudMigration.FrontendPollInterval = cloudMigration.Key("frontend_poll_interval").MustDuration(2 * time.Second) cfg.CloudMigration.AlertRulesState = cloudMigration.Key("alert_rules_state").In(GMSAlertRulesPaused, []string{GMSAlertRulesPaused, GMSAlertRulesUnchanged}) diff --git a/pkg/setting/setting_jwt.go b/pkg/setting/setting_jwt.go index 2a559a145b7..18c7866cccf 100644 --- a/pkg/setting/setting_jwt.go +++ b/pkg/setting/setting_jwt.go @@ -1,6 +1,10 @@ package setting -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/util" +) const ( extJWTAccessTokenExpectAudience = "grafana" @@ -22,6 +26,8 @@ type AuthJWTSettings struct { AutoSignUp bool RoleAttributePath string RoleAttributeStrict bool + OrgMapping []string + OrgAttributePath string AllowAssignGrafanaAdmin bool SkipOrgRoleSync bool GroupsAttributePath string @@ -71,6 +77,8 @@ func (cfg *Cfg) readAuthJWTSettings() { jwtSettings.EmailAttributePath = valueAsString(authJWT, "email_attribute_path", "") jwtSettings.UsernameAttributePath = valueAsString(authJWT, "username_attribute_path", "") jwtSettings.TlsSkipVerify = authJWT.Key("tls_skip_verify_insecure").MustBool(false) + jwtSettings.OrgAttributePath = valueAsString(authJWT, "org_attribute_path", "") + jwtSettings.OrgMapping = util.SplitString(valueAsString(authJWT, "org_mapping", "")) cfg.JWTAuth = jwtSettings } diff --git a/pkg/setting/setting_k8s_dashboard_cleanup.go b/pkg/setting/setting_k8s_dashboard_cleanup.go new file mode 100644 index 00000000000..660de43c388 --- /dev/null +++ b/pkg/setting/setting_k8s_dashboard_cleanup.go @@ -0,0 +1,53 @@ +package setting + +import ( + "time" +) + +type K8sDashboardCleanupSettings struct { + Interval time.Duration + Timeout time.Duration + BatchSize int64 +} + +const ( + defaultK8sDashboardCleanupInterval = 30 * time.Second + defaultK8sDashboardCleanupBatchSize = int64(10) + minK8sDashboardCleanupInterval = 10 * time.Second + minK8sDashboardCleanupTimeout = 5 * time.Second + minK8sDashboardCleanupBatchSize = int64(5) + maxK8sDashboardCleanupBatchSize = int64(200) +) + +func (cfg *Cfg) readK8sDashboardCleanupSettings() { + section := cfg.Raw.Section("dashboard_cleanup") + + // Read interval setting with validation + cleanupInterval := section.Key("interval").MustDuration(defaultK8sDashboardCleanupInterval) + if cleanupInterval < minK8sDashboardCleanupInterval { + cfg.Logger.Warn("[dashboard_cleanup.interval] is too low; the minimum allowed (10s) is enforced") + cleanupInterval = minK8sDashboardCleanupInterval + } + + // Calculate timeout as 5 seconds less than interval, with minimum validation + cleanupTimeout := cleanupInterval - (5 * time.Second) + if cleanupTimeout < minK8sDashboardCleanupTimeout { + cleanupTimeout = minK8sDashboardCleanupTimeout + } + + // Read batch size with validation + batchSize := section.Key("batch_size").MustInt64(defaultK8sDashboardCleanupBatchSize) + if batchSize < minK8sDashboardCleanupBatchSize { + cfg.Logger.Warn("[dashboard_cleanup.batch_size] is too low; the minimum allowed (5) is enforced") + batchSize = minK8sDashboardCleanupBatchSize + } else if batchSize > maxK8sDashboardCleanupBatchSize { + cfg.Logger.Warn("[dashboard_cleanup.batch_size] is too high; the maximum allowed (1000) is enforced") + batchSize = maxK8sDashboardCleanupBatchSize + } + + cfg.K8sDashboardCleanup = K8sDashboardCleanupSettings{ + Interval: cleanupInterval, + Timeout: cleanupTimeout, + BatchSize: batchSize, + } +} diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index b14d22b97bc..84461d3c82f 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -112,6 +112,7 @@ type UnifiedAlertingSettings struct { StateHistory UnifiedAlertingStateHistorySettings RemoteAlertmanager RemoteAlertmanagerSettings RecordingRules RecordingRuleSettings + PrometheusConversion UnifiedAlertingPrometheusConversionSettings // MaxStateSaveConcurrency controls the number of goroutines (per rule) that can save alert state in parallel. MaxStateSaveConcurrency int @@ -165,6 +166,12 @@ type UnifiedAlertingReservedLabelSettings struct { DisabledLabels map[string]struct{} } +// UnifiedAlertingPrometheusConversionSettings contains configuration for converting Prometheus rules to Grafana format +type UnifiedAlertingPrometheusConversionSettings struct { + // RuleQueryOffset defines a time offset to apply to rule queries during conversion from Prometheus to Grafana format + RuleQueryOffset time.Duration +} + type UnifiedAlertingStateHistorySettings struct { Enabled bool Backend string @@ -437,6 +444,11 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { } uaCfg.StateHistory = uaCfgStateHistory + prometheusConversion := iniFile.Section("unified_alerting.prometheus_conversion") + uaCfg.PrometheusConversion = UnifiedAlertingPrometheusConversionSettings{ + RuleQueryOffset: prometheusConversion.Key("rule_query_offset").MustDuration(time.Minute), + } + rr := iniFile.Section("recording_rules") uaCfgRecordingRules := RecordingRuleSettings{ Enabled: rr.Key("enabled").MustBool(false), diff --git a/pkg/storage/legacysql/dualwrite/dualwriter.go b/pkg/storage/legacysql/dualwrite/dualwriter.go index b2389d3d8b6..7e3c955fa26 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter.go @@ -3,6 +3,7 @@ package dualwrite import ( "context" "fmt" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -12,6 +13,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana-app-sdk/logging" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -19,6 +21,8 @@ var ( _ grafanarest.Storage = (*dualWriter)(nil) ) +const backgroundReqTimeout = 5 * time.Second + // dualWriter will write first to legacy, then to unified keeping the same internal ID type dualWriter struct { legacy grafanarest.Storage @@ -29,35 +33,60 @@ type dualWriter struct { } func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - // Call get (send read traffic in cloud) - unifiedGet, unifiedErr := d.unified.Get(ctx, name, options) + // If we read from unified, we can just do that and return. if d.readUnified { - return unifiedGet, unifiedErr + return d.unified.Get(ctx, name, options) } - + // If legacy is still our main store, lets first read from it. legacyGet, err := d.legacy.Get(ctx, name, options) if err != nil { return nil, err } - - if unifiedErr != nil && !apierrors.IsNotFound(unifiedErr) && !d.errorIsOK { - return nil, unifiedErr // the unified error + // Once we have successfully read from legacy, we can check if we want to fail on a unified read. + // If we allow the unified read to fail, we can do it in the background. + if d.errorIsOK { + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, err := d.unified.Get(ctxBg, name, options); err != nil { + d.log.Error("failed background GET to unified", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + return legacyGet, nil + } + // If it's not okay to fail, we have to check it in the foreground. + _, unifiedErr := d.unified.Get(ctx, name, options) + if unifiedErr != nil && !apierrors.IsNotFound(unifiedErr) { + return nil, unifiedErr } return legacyGet, nil } func (d *dualWriter) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - // Call list (send read traffic in cloud) - unifiedList, err := d.unified.List(ctx, options) + // If we read from unified, we can just do that and return. if d.readUnified { - return unifiedList, err + return d.unified.List(ctx, options) } - - if err != nil && !d.errorIsOK { + // If legacy is still the main store, lets first read from it. + legacyList, err := d.legacy.List(ctx, options) + if err != nil { return nil, err } - - return d.legacy.List(ctx, options) + // Once we have successfully listed from legacy, we can check if we want to fail on a unified list. + // If we allow the unified list to fail, we can do it in the background and return. + if d.errorIsOK { + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, err := d.unified.List(ctxBg, options); err != nil { + d.log.Error("failed background LIST to unified", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + return legacyList, nil + } + // If it's not okay to fail, we have to check it in the foreground. + if _, err := d.unified.List(ctx, options); err != nil { + return nil, err + } + return legacyList, nil } // Create overrides the behavior of the generic DualWriter and writes to LegacyStorage and Storage. @@ -93,23 +122,42 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida accCreated.SetResourceVersion("") accCreated.SetUID("") - storageObj, errObjectSt := d.unified.Create(ctx, createdCopy, createValidation, options) - if errObjectSt != nil { - log.Error("unable to create object in unified storage", "err", errObjectSt) - if d.errorIsOK { - return createdFromLegacy, nil - } - - // if we cannot create in unistore, attempt to clean up legacy - _, _, err = d.legacy.Delete(ctx, accCreated.GetName(), nil, &metav1.DeleteOptions{}) - if err != nil { - log.Error("unable to cleanup object in legacy storage", "err", err) - } - return nil, errObjectSt - } - + // If unified storage is the primary storage, let's just create it in the foreground and return it. if d.readUnified { + storageObj, errObjectSt := d.unified.Create(ctx, createdCopy, createValidation, options) + if errObjectSt != nil { + log.Error("unable to create object in unified storage", "err", errObjectSt) + // If we cannot create in unified storage, attempt to clean up legacy. + _, _, err = d.legacy.Delete(ctx, accCreated.GetName(), nil, &metav1.DeleteOptions{}) + if err != nil { + log.Error("unable to cleanup object in legacy storage", "err", err) + } + return nil, errObjectSt + } return storageObj, nil + } else if d.errorIsOK { + // If we don't use unified as the primary store and errors are okay, let's create it in the background. + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, err := d.unified.Create(ctxBg, createdCopy, createValidation, options); err != nil { + log.Error("unable to create object in unified storage", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + } else { + // Otherwise let's create it in the foreground and return any error. + if _, err := d.unified.Create(ctx, createdCopy, createValidation, options); err != nil { + log.Error("unable to create object in unified storage", "err", err) + if d.errorIsOK { + return createdFromLegacy, nil + } + + // If we cannot create in unified storage, attempt to clean up legacy. + _, _, errLegacy := d.legacy.Delete(ctx, accCreated.GetName(), nil, &metav1.DeleteOptions{}) + if errLegacy != nil { + log.Error("unable to cleanup object in legacy storage", "err", errLegacy) + } + return nil, err + } } return createdFromLegacy, nil } @@ -125,16 +173,28 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r if err != nil && (!d.readUnified || !d.errorIsOK && !apierrors.IsNotFound(err)) { return nil, false, err } - - objFromStorage, asyncStorage, err := d.unified.Delete(ctx, name, deleteValidation, options) + // If unified storage is our primary store, just delete it and return + if d.readUnified { + objFromStorage, asyncStorage, err := d.unified.Delete(ctx, name, deleteValidation, options) + if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { + return nil, false, err + } + return objFromStorage, asyncStorage, nil + } else if d.errorIsOK { + // If errors are okay and unified is not primary, we can just run it as background operation. + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + _, _, err := d.unified.Delete(ctxBg, name, deleteValidation, options) + if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { + d.log.Error("failed background DELETE in unified storage", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + } + // Otherwise we just run it in the foreground and return an error if any might happen. + _, _, err = d.unified.Delete(ctx, name, deleteValidation, options) if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { return nil, false, err } - - if d.readUnified { - return objFromStorage, asyncStorage, nil - } - return objFromLegacy, asyncLegacy, nil } @@ -157,20 +217,23 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat log.With("object", objFromLegacy).Error("could not update in legacy storage", "err", err) return nil, false, err } - - objFromStorage, created, err := d.unified.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err != nil { - log.With("object", objFromStorage).Error("could not update in storage", "err", err) - if d.errorIsOK { - return objFromLegacy, createdLegacy, nil - } + // If unified storage is our primary store, just update it there and return. + if d.readUnified { + return d.unified.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) + } else if d.errorIsOK { + // If unified is not primary, but errors are okay, we can just run in the background. + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, _, err := d.unified.Update(ctxBg, name, objInfo, createValidation, updateValidation, forceAllowCreate, options); err != nil { + log.Error("failed background UPDATE to unified storage", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + return objFromLegacy, createdLegacy, nil + } + // If we want to check unified errors just run it in foreground. + if _, _, err := d.unified.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options); err != nil { return nil, false, err } - - if d.readUnified { - return objFromStorage, created, nil - } - return objFromLegacy, createdLegacy, nil } @@ -190,19 +253,24 @@ func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest return nil, err } - deletedStorage, err := d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions) - if err != nil { + // If unified is the primary store, we can just delete it there and return. + if d.readUnified { + return d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions) + } else if d.errorIsOK { + // If unified storage is not the primary store and errors are okay, we can just run it in the background. + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, err := d.unified.DeleteCollection(ctxBg, deleteValidation, options, listOptions); err != nil { + log.Error("failed background DELETE collection to unified storage", "err", err) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + return deletedLegacy, nil + } + // Otherwise we have to check the error and run it in the foreground. + if deletedStorage, err := d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions); err != nil { log.With("deleted", deletedStorage).Error("failed to delete collection successfully from Storage", "err", err) - if d.errorIsOK { - return deletedLegacy, nil - } return nil, err } - - if d.readUnified { - return deletedStorage, nil - } - return deletedLegacy, nil } diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go index 74a73a573c2..0de214c4596 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go @@ -129,6 +129,15 @@ func TestMode1_Get(t *testing.T) { m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) }, }, + { + name: "should not block for unified storage", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).WaitUntil(time.After(time.Hour)).Return(anotherObj, nil) + }, + }, } name := "foo" diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go new file mode 100644 index 00000000000..ebc58801fd8 --- /dev/null +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -0,0 +1,48 @@ +package metadata + +import ( + "context" + + claims "github.com/grafana/authlib/types" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" +) + +func ProvideKeeperMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.KeeperMetadataStorage, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || + !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { + return &keeperMetadataStorage{}, nil + } + + return &keeperMetadataStorage{db: db, accessClient: accessClient}, nil +} + +// keeperMetadataStorage is the actual implementation of the keeper metadata storage. +type keeperMetadataStorage struct { + db db.DB + accessClient claims.AccessClient +} + +func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) { + return nil, nil +} + +func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.Keeper, error) { + return nil, nil +} + +func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) { + return nil, nil +} + +func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { + return nil +} + +func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.KeeperList, error) { + return nil, nil +} diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go new file mode 100644 index 00000000000..1c55a430548 --- /dev/null +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "context" + + claims "github.com/grafana/authlib/types" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" +) + +func ProvideSecureValueMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.SecureValueMetadataStorage, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || + !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { + return &secureValueMetadataStorage{}, nil + } + + return &secureValueMetadataStorage{db: db, accessClient: accessClient}, nil +} + +// secureValueMetadataStorage is the actual implementation of the secure value metadata storage. +type secureValueMetadataStorage struct { + db db.DB + accessClient claims.AccessClient +} + +func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) { + return nil, nil +} + +func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) { + return nil, nil +} + +func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) { + return nil, nil +} + +func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { + return nil +} + +func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.SecureValueList, error) { + return nil, nil +} diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index d19d679b77c..8efde1c2c91 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -203,37 +203,6 @@ then run: kubectl --kubeconfig=./grafana.kubeconfig create -f folder-generate.yaml ``` -### Use a separate database - -By default Unified Storage uses the Grafana database. To run against a separate database, update `custom.ini` by adding the following section to it: - -``` -[resource_api] -db_type = mysql -db_host = localhost:3306 -db_name = grafana -db_user = -db_pass = -``` - -MySQL and Postgres are both supported. The `` and `` values can be found in the following devenv docker compose files: [MySQL](https://github.com/grafana/grafana/blob/main/devenv/docker/blocks/mysql/docker-compose.yaml#L6-L7) and [Postgres](https://github.com/grafana/grafana/blob/main/devenv/docker/blocks/postgres/docker-compose.yaml#L4-L5). - -Then, run -```sh -make devenv sources= -``` -where source is either `mysql` or `postgres`. - -Finally, run the Grafana backend with - -```sh -bra run -``` -or -```sh -make run -``` - ### Run as a GRPC service #### Start GRPC storage-server @@ -284,23 +253,24 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - make changes in `.proto` file - to compile all protobuf files in the repository run `make protobuf` at its top level -## Setting up search (EXPERIMENTAL) -Unified storage now exposes an **experimental** search API. It can be used to search for specific resources, or to filter/query resources. +## Setting up search To enable it, add the following to your `custom.ini` under the `[feature_toggles]` section: ```ini [feature_toggles] +; Used by the Grafana instance +unifiedStorageSearchUI = true +kubernetesClientDashboardsFolders = true + +; Used by unified storage unifiedStorageSearch = true +; (optional) Allows you to sort dashboards by usage insights fields when using enterprise +; unifiedStorageSearchSprinkles = true +; (optional) Will skip search results filter based on user permissions +; unifiedStorageSearchPermissionFiltering = false ``` -To access the api through Grafana, go to Explore -> Query Type -> Search. +The dashboard search page has been set up to search unified storage. Additionally, all legacy search calls (e.g. `/api/search`) will go to +unified storage when the dual writer mode is set to 3 or greater. When <= 2, the legacy search api calls will go to legacy storage. -The query needs to be a valid [Bleve query string](https://blevesearch.com/docs/Query-String-Query/). - -Some example queries are: -- `*` - returns all objects -- `Kind:Playlist` - returns all playlists -- `Spec.inveral:5m` - returns all objects with the spec.inverval field set to 5m -- `+Kind:Playlist +Spec.title:p4` - returns all playlists with the title matching "p4" -- `*foo*` - returns all objects containing "foo" in any field -- `CreatedAt:>="2024-10-17"` - returns all objects created after 2024-10-17 -- `+CreatedAt:>="2024-10-17" +Kind:Playlist` - returns all playlists created after 2024-10-17 +## Running load tests +Load tests and instructions can be found [here](https://github.com/grafana/grafana-api-tests/tree/main/simulation/src/unified_storage). diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 0bf35029b66..f0b502e45eb 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/storage/unified/apistore -go 1.23.7 +go 1.24.1 replace ( github.com/grafana/grafana => ../../../.. @@ -17,13 +17,13 @@ require ( github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250317130411-3f270d1de043 github.com/stretchr/testify v1.10.0 gocloud.dev v0.40.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - google.golang.org/grpc v1.70.0 + google.golang.org/grpc v1.71.0 k8s.io/apimachinery v0.32.1 k8s.io/apiserver v0.32.1 k8s.io/client-go v0.32.1 @@ -51,32 +51,30 @@ require ( github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect - github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/RoaringBitmap/roaring v1.9.3 // indirect + github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect github.com/Yiling-J/theine-go v0.6.0 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/apache/thrift v0.21.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect - github.com/aws/aws-sdk-go v1.55.5 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect @@ -101,24 +99,24 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/blevesearch/bleve/v2 v2.4.3 // indirect - github.com/blevesearch/bleve_index_api v1.1.12 // indirect + github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 // indirect + github.com/blevesearch/bleve_index_api v1.2.3 // indirect github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.23 // indirect + github.com/blevesearch/go-faiss v1.0.24 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.5 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.0.10 // indirect - github.com/blevesearch/zapx/v11 v11.3.10 // indirect - github.com/blevesearch/zapx/v12 v12.3.10 // indirect - github.com/blevesearch/zapx/v13 v13.3.10 // indirect - github.com/blevesearch/zapx/v14 v14.3.10 // indirect - github.com/blevesearch/zapx/v15 v15.3.16 // indirect - github.com/blevesearch/zapx/v16 v16.1.8 // indirect + github.com/blevesearch/vellum v1.1.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.1 // indirect + github.com/blevesearch/zapx/v12 v12.4.1 // indirect + github.com/blevesearch/zapx/v13 v13.4.1 // indirect + github.com/blevesearch/zapx/v14 v14.4.1 // indirect + github.com/blevesearch/zapx/v15 v15.4.1 // indirect + github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bufbuild/protocompile v0.4.0 // indirect @@ -128,7 +126,7 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect @@ -147,9 +145,9 @@ require ( github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fullstorydev/grpchan v1.1.1 // indirect @@ -177,13 +175,13 @@ require ( github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -192,7 +190,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.23.2 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -212,7 +210,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.272.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect @@ -254,8 +252,8 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -313,7 +311,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -366,12 +364,12 @@ require ( go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect @@ -390,7 +388,7 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect @@ -399,8 +397,8 @@ require ( gonum.org/v1/gonum v0.15.1 // indirect google.golang.org/api v0.220.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 1466cc1def3..09dfe3de471 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -649,8 +649,9 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= @@ -667,7 +668,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -688,8 +688,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= -github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= +github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0= @@ -713,8 +713,8 @@ github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOL github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -734,8 +734,8 @@ github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -790,42 +790,42 @@ github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZ github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4= -github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY= -github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY= -github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 h1:u6DQJ1k4FKwRNtsrVhIRQenNdtz31way7/LgWCluFzA= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205/go.mod h1:nSmFOQ7M264rKoM3jf63Gl2G+ylCgZGovPgL6ZEQYzU= +github.com/blevesearch/bleve_index_api v1.2.3 h1:aJJPhTb30zc68qg2HbvCZLAF1Eh5Hhm296mDAUFaUeg= +github.com/blevesearch/bleve_index_api v1.2.3/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc= -github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/go-faiss v1.0.24 h1:K79IvKjoKHdi7FdiXEsAhxpMuns0x4fM0BO93bW5jLI= +github.com/blevesearch/go-faiss v1.0.24/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= +github.com/blevesearch/scorch_segment_api/v2 v2.3.5 h1:t6NVTdS8xp1xHMJoe5WjxdZWLKhsIIb6kB0U49rhAno= +github.com/blevesearch/scorch_segment_api/v2 v2.3.5/go.mod h1:7gG1vsL3lu2L+7RuEtZ7PJhxGYE+AV9zjxTSHuXFMdM= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= -github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= -github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= -github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= -github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= -github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= -github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= -github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= -github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= -github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= -github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= -github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= -github.com/blevesearch/zapx/v16 v16.1.8 h1:Bxzpw6YQpFs7UjoCV1+RvDw6fmAT2GZxldwX8b3wVBM= -github.com/blevesearch/zapx/v16 v16.1.8/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8= +github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= +github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= +github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= +github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= +github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= +github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= +github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= +github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= +github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= +github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= +github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= +github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= +github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9 h1:a5GLOQkJqCnpD/lTfNiEVQnJ1e2CVOR8JFO6u1D7bLE= +github.com/blevesearch/zapx/v16 v16.2.2-0.20250305220028-89edb0ef9aa9/go.mod h1:v8B9VjBtELr2qRO0f3RoRG8RAZg3Jml/5SYuBT0hvGw= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= @@ -874,8 +874,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -962,8 +962,8 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= -github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -975,8 +975,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2T github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -1070,8 +1070,8 @@ github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuq github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -1086,10 +1086,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -1147,8 +1147,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -1268,8 +1268,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 h1:wdJy5x6M7auWDjUIubqhfZuZvphUMyjD7hxB3RqV4aE= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go.mod h1:jwYig4wlnLLq4HQKDpS95nDeZi4+DmcD17KYYS1gMJg= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= @@ -1412,11 +1412,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1602,8 +1602,8 @@ github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1843,8 +1843,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= @@ -1853,8 +1853,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= @@ -2208,8 +2208,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -2553,10 +2553,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2601,8 +2601,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2660,6 +2660,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index c9677fd0763..e6d9458ebfb 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -9,13 +9,14 @@ import ( "time" "github.com/google/uuid" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/storage" "k8s.io/klog/v2" authtypes "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -57,6 +58,9 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime if obj.GetUID() == "" { obj.SetUID(types.UID(uuid.NewString())) } + if obj.GetFolder() != "" && !s.opts.EnableFolderSupport { + return nil, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String())) + } if s.opts.RequireDeprecatedInternalID { // nolint:staticcheck @@ -77,6 +81,7 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime obj.SetUpdatedBy("") obj.SetUpdatedTimestamp(nil) obj.SetCreatedBy(info.GetUID()) + obj.SetGeneration(1) // the first time we write var buf bytes.Buffer if err = s.codec.Encode(newObject, &buf); err != nil { @@ -131,8 +136,35 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti obj.SetDeprecatedInternalID(previousInternalID) // nolint:staticcheck } - obj.SetUpdatedBy(info.GetUID()) - obj.SetUpdatedTimestampMillis(time.Now().UnixMilli()) + // Check if we should bump the generation + changed := obj.GetFolder() != previous.GetFolder() + if changed { + if !s.opts.EnableFolderSupport { + return nil, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String())) + } + // TODO: check that we can move the folder? + } else if obj.GetDeletionTimestamp() != nil && previous.GetDeletionTimestamp() == nil { + changed = true // bump generation when deleted + } else { + spec, e1 := obj.GetSpec() + oldSpec, e2 := previous.GetSpec() + if e1 == nil && e2 == nil { + if !apiequality.Semantic.DeepEqual(spec, oldSpec) { + changed = true + } + } + } + + // Mark the resource as changed + if changed { + obj.SetGeneration(previous.GetGeneration() + 1) + obj.SetUpdatedBy(info.GetUID()) + obj.SetUpdatedTimestampMillis(time.Now().UnixMilli()) + } else { + obj.SetGeneration(previous.GetGeneration()) + obj.SetAnnotation(utils.AnnoKeyUpdatedBy, previous.GetAnnotation(utils.AnnoKeyUpdatedBy)) + obj.SetAnnotation(utils.AnnoKeyUpdatedTimestamp, previous.GetAnnotation(utils.AnnoKeyUpdatedTimestamp)) + } var buf bytes.Buffer if err = s.codec.Encode(updateObject, &buf); err != nil { diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go index 1bb9c06cd04..1c17a9806c2 100644 --- a/pkg/storage/unified/apistore/prepare_test.go +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/exp/rand" "k8s.io/apimachinery/pkg/api/apitesting" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/storage" @@ -30,11 +32,14 @@ func TestPrepareObjectForStorage(t *testing.T) { codec: apitesting.TestCodec(codecs, v0alpha1.DashboardResourceInfo.GroupVersion()), snowflake: node, opts: StorageOptions{ - LargeObjectSupport: nil, + EnableFolderSupport: true, + LargeObjectSupport: nil, }, } - ctx := authtypes.WithAuthInfo(context.Background(), &identity.StaticRequester{UserID: 1, UserUID: "user-uid", Type: authtypes.TypeUser}) + ctx := authtypes.WithAuthInfo(context.Background(), + &identity.StaticRequester{UserID: 1, UserUID: "user-uid", Type: authtypes.TypeUser}, + ) t.Run("Error getting auth info from context", func(t *testing.T) { _, err := s.prepareObjectForStorage(context.Background(), nil) @@ -81,7 +86,7 @@ func TestPrepareObjectForStorage(t *testing.T) { require.Empty(t, updatedTS) }) - t.Run("Should keep repo info", func(t *testing.T) { + t.Run("Should keep manager info", func(t *testing.T) { dashboard := v0alpha1.Dashboard{} dashboard.Name = "test-name" obj := dashboard.DeepCopyObject() @@ -117,6 +122,65 @@ func TestPrepareObjectForStorage(t *testing.T) { require.Equal(t, s.TimestampMillis, now.UnixMilli()) }) + t.Run("Update should manage incrementing generation and metadata", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + obj := dashboard.DeepCopyObject() + meta, err := utils.MetaAccessor(obj) + meta.SetFolder("aaa") + require.NoError(t, err) + + encodedData, err := s.prepareObjectForStorage(ctx, obj) + require.NoError(t, err) + + insertedObject, _, err := s.codec.Decode(encodedData, nil, &v0alpha1.Dashboard{}) + require.NoError(t, err) + meta, err = utils.MetaAccessor(insertedObject) + require.NoError(t, err) + require.Equal(t, int64(1), meta.GetGeneration()) + require.Equal(t, "user:user-uid", meta.GetCreatedBy()) + require.Equal(t, "", meta.GetUpdatedBy()) // empty + ts, err := meta.GetUpdatedTimestamp() + require.NoError(t, err) + require.Nil(t, ts) + + // Change the user... and only update metadata + ctx = authtypes.WithAuthInfo(context.Background(), + &identity.StaticRequester{UserID: 1, UserUID: "user2", Type: authtypes.TypeUser}, + ) + + // Change the status... but generation is the same + updatedObject := insertedObject.DeepCopyObject() + meta, err = utils.MetaAccessor(updatedObject) + require.NoError(t, err) + err = meta.SetStatus(v0alpha1.DashboardStatus{ + Conversion: &v0alpha1.DashboardConversionStatus{ + Failed: true, + Error: "test", + }, + }) + require.NoError(t, err) + meta.SetGeneration(123) // will be removed + + // Update status without changing generation or update metadata + _, err = s.prepareObjectForUpdate(ctx, updatedObject, insertedObject) + require.NoError(t, err) + require.Equal(t, "", meta.GetUpdatedBy()) + require.Equal(t, int64(1), meta.GetGeneration()) + + // Change the folder -- the generation should increase and the updatedBy metadata + dashboard2 := &v0alpha1.Dashboard{ObjectMeta: v1.ObjectMeta{ + Name: dashboard.Name, + }} // TODO... deep copy, See: https://github.com/grafana/grafana/pull/102258 + meta2, err := utils.MetaAccessor(dashboard2) + require.NoError(t, err) + meta2.SetFolder("xyz") // will bump generation + _, err = s.prepareObjectForUpdate(ctx, dashboard2, updatedObject) + require.NoError(t, err) + require.Equal(t, "user:user2", meta2.GetUpdatedBy()) + require.Equal(t, int64(2), meta2.GetGeneration()) + }) + s.opts.RequireDeprecatedInternalID = true t.Run("Should generate internal id", func(t *testing.T) { dashboard := v0alpha1.Dashboard{} @@ -149,4 +213,87 @@ func TestPrepareObjectForStorage(t *testing.T) { require.NoError(t, err) require.Equal(t, meta.GetDeprecatedInternalID(), int64(1)) // nolint:staticcheck }) + + t.Run("calculate generation", func(t *testing.T) { + dash := &v0alpha1.Dashboard{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + }, + Spec: v0alpha1.DashboardSpec{ + Object: map[string]interface{}{ + "hello": "world", + }, + }, + } + out := getPreparedObject(t, ctx, s, dash, nil) + require.Equal(t, int64(1), out.GetGeneration()) + require.NotEmpty(t, out.GetAnnotation(utils.AnnoKeyCreatedBy)) + require.Equal(t, "", out.GetAnnotation(utils.AnnoKeyUpdatedBy)) + require.Equal(t, "", out.GetAnnotation(utils.AnnoKeyUpdatedTimestamp)) + + t.Run("increment when the spec changes", func(t *testing.T) { + b := dash.DeepCopy() + b.Spec.Object["x"] = "y" + out = getPreparedObject(t, ctx, s, b, dash) + require.Equal(t, int64(2), out.GetGeneration()) + require.NotEmpty(t, out.GetAnnotation(utils.AnnoKeyUpdatedBy)) + require.NotEmpty(t, out.GetAnnotation(utils.AnnoKeyUpdatedTimestamp)) + }) + + t.Run("increment when the folder changes", func(t *testing.T) { + b := dash.DeepCopy() + b.Annotations = map[string]string{ + utils.AnnoKeyFolder: "abc", + } + out = getPreparedObject(t, ctx, s, b, dash) + require.Equal(t, int64(2), out.GetGeneration()) + }) + + t.Run("increment when deleted", func(t *testing.T) { + now := v1.Now() + b := dash.DeepCopy() + b.DeletionTimestamp = &now + out = getPreparedObject(t, ctx, s, b, dash) + require.Equal(t, int64(2), out.GetGeneration()) + }) + + t.Run("keep when status, labels, or annotations change", func(t *testing.T) { + b := dash.DeepCopy() + b.Annotations = map[string]string{ + "x": "hello", + } + b.Labels = map[string]string{ + "a": "b", + } + b.Status = v0alpha1.DashboardStatus{ + Conversion: &v0alpha1.DashboardConversionStatus{ + Failed: true, + }, + } + out = getPreparedObject(t, ctx, s, b, dash) + require.Equal(t, int64(1), out.GetGeneration()) // still 1 + }) + }) +} + +func getPreparedObject(t *testing.T, ctx context.Context, s *Storage, obj runtime.Object, old runtime.Object) utils.GrafanaMetaAccessor { + t.Helper() + + var raw []byte + var err error + + if old == nil { + raw, err = s.prepareObjectForStorage(ctx, obj) + } else { + raw, err = s.prepareObjectForUpdate(ctx, obj, old) + } + require.NoError(t, err) + + out := &unstructured.Unstructured{} + err = out.UnmarshalJSON(raw) + require.NoError(t, err) + + meta, err := utils.MetaAccessor(out) + require.NoError(t, err) + return meta } diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index 69fc83f803c..4557c0a5d39 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -47,8 +47,14 @@ var _ storage.Interface = (*Storage)(nil) // Optional settings that apply to a single resource type StorageOptions struct { + // ????: should we constrain this to only dashboards for now? + // Not yet clear if this is a good general solution, or just a stop-gap LargeObjectSupport LargeObjectSupport + // Allow writing objects with metadata.annotations[grafana.app/folder] + EnableFolderSupport bool + + // Add internalID label when missing RequireDeprecatedInternalID bool } @@ -445,11 +451,11 @@ func (s *Storage) GuaranteedUpdate( cachedExistingObject runtime.Object, ) error { var ( - res storage.ResponseMeta - updatedObj runtime.Object - existingObj runtime.Object - created bool - err error + res storage.ResponseMeta + updatedObj runtime.Object + existingObj runtime.Object + existingBytes []byte + err error ) req := &resource.UpdateRequest{} req.Key, err = s.getKey(key) @@ -465,61 +471,71 @@ func (s *Storage) GuaranteedUpdate( for attempt := 1; attempt <= MaxUpdateAttempts; attempt = attempt + 1 { // Read the latest value - rsp, err := s.store.Read(ctx, &resource.ReadRequest{Key: req.Key}) + readResponse, err := s.store.Read(ctx, &resource.ReadRequest{Key: req.Key}) if err != nil { return resource.GetError(resource.AsErrorResult(err)) } - if rsp.Error != nil { - if rsp.Error.Code == http.StatusNotFound { + if readResponse.Error != nil { + if readResponse.Error.Code == http.StatusNotFound { if !ignoreNotFound { return apierrors.NewNotFound(s.gr, req.Key.Name) } } else { - return resource.GetError(rsp.Error) + return resource.GetError(readResponse.Error) } } - created = true - existingObj = s.newFunc() - if len(rsp.Value) > 0 { - created = false - _, err = s.convertToObject(rsp.Value, existingObj) + // Upsert? (create because it does not already exist) + if len(readResponse.Value) == 0 { + if !ignoreNotFound { + return apierrors.NewNotFound(s.gr, req.Key.Name) + } + + updatedObj, _, err = tryUpdate(s.newFunc(), res) if err != nil { - return err - } - - mmm, err := utils.MetaAccessor(existingObj) - if err != nil { - return err - } - mmm.SetResourceVersionInt64(rsp.ResourceVersion) - res.ResourceVersion = uint64(rsp.ResourceVersion) - - if rest.IsDualWriteUpdate(ctx) { - // Ignore the RV when updating legacy values - mmm.SetResourceVersion("") - } else { - if err := preconditions.Check(key, existingObj); err != nil { - if attempt >= MaxUpdateAttempts { - return fmt.Errorf("precondition failed: %w", err) - } - continue - } - } - - // restore the full original object before tryUpdate - if s.opts.LargeObjectSupport != nil && mmm.GetBlob() != nil { - err = s.opts.LargeObjectSupport.Reconstruct(ctx, req.Key, s.store, mmm) - if err != nil { + if attempt >= MaxUpdateAttempts { return err } + continue } - } else if !ignoreNotFound { - return apierrors.NewNotFound(s.gr, req.Key.Name) + return s.Create(ctx, key, updatedObj, destination, 0) } - updatedObj, _, err = tryUpdate(existingObj.DeepCopyObject(), res) + existingBytes = readResponse.Value + existingObj, err = s.convertToObject(readResponse.Value, s.newFunc()) + if err != nil { + return err + } + + existing, err := utils.MetaAccessor(existingObj) + if err != nil { + return err + } + existing.SetResourceVersionInt64(readResponse.ResourceVersion) + res.ResourceVersion = uint64(readResponse.ResourceVersion) + + if rest.IsDualWriteUpdate(ctx) { + // Ignore the RV when updating legacy values + existing.SetResourceVersion("") + } else { + if err := preconditions.Check(key, existingObj); err != nil { + if attempt >= MaxUpdateAttempts { + return fmt.Errorf("precondition failed: %w", err) + } + continue + } + } + + // restore the full original object before tryUpdate + if s.opts.LargeObjectSupport != nil && existing.GetBlob() != nil { + err = s.opts.LargeObjectSupport.Reconstruct(ctx, req.Key, s.store, existing) + if err != nil { + return err + } + } + + updatedObj, _, err = tryUpdate(existingObj, res) if err != nil { if attempt >= MaxUpdateAttempts { return err @@ -529,64 +545,32 @@ func (s *Storage) GuaranteedUpdate( break } - unchanged, err := isUnchanged(s.codec, existingObj, updatedObj) + req.Value, err = s.prepareObjectForUpdate(ctx, updatedObj, existingObj) if err != nil { return err } - if unchanged { - var buf bytes.Buffer - if err = s.codec.Encode(updatedObj, &buf); err != nil { - return err - } - if _, err := s.convertToObject(buf.Bytes(), destination); err != nil { - return err - } - return nil - } - - var ( - value []byte - rv int64 - ) - if created { - value, err = s.prepareObjectForStorage(ctx, updatedObj) - if err != nil { - return err - } - rsp2, err := s.store.Create(ctx, &resource.CreateRequest{ - Key: req.Key, - Value: value, - }) + var rv uint64 + // Only update (for real) if the bytes have changed + if !bytes.Equal(req.Value, existingBytes) { + updateResponse, err := s.store.Update(ctx, req) if err != nil { return resource.GetError(resource.AsErrorResult(err)) } - if rsp2.Error != nil { - return resource.GetError(rsp2.Error) + if updateResponse.Error != nil { + return resource.GetError(updateResponse.Error) } - rv = rsp2.ResourceVersion - } else { - value, err = s.prepareObjectForUpdate(ctx, updatedObj, existingObj) - if err != nil { + rv = uint64(updateResponse.ResourceVersion) + } + + if _, err := s.convertToObject(req.Value, destination); err != nil { + return err + } + + if rv > 0 { + if err := s.versioner.UpdateObject(destination, rv); err != nil { return err } - req.Value = value - rsp2, err := s.store.Update(ctx, req) - if err != nil { - return resource.GetError(resource.AsErrorResult(err)) - } - if rsp2.Error != nil { - return resource.GetError(rsp2.Error) - } - rv = rsp2.ResourceVersion - } - - if _, err := s.convertToObject(value, destination); err != nil { - return err - } - - if err := s.versioner.UpdateObject(destination, uint64(rv)); err != nil { - return err } return nil diff --git a/pkg/storage/unified/apistore/util.go b/pkg/storage/unified/apistore/util.go index e41a50b5efa..44ed68d0b68 100644 --- a/pkg/storage/unified/apistore/util.go +++ b/pkg/storage/unified/apistore/util.go @@ -6,13 +6,11 @@ package apistore import ( - "bytes" "fmt" "strconv" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/selection" "k8s.io/apiserver/pkg/storage" @@ -31,6 +29,27 @@ func toListRequest(k *resource.ResourceKey, opts storage.ListOptions) (*resource NextPageToken: predicate.Continue, } + if opts.ResourceVersion != "" { + rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64) + if err != nil { + return nil, predicate, apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion)) + } + req.ResourceVersion = rv + } + + switch opts.ResourceVersionMatch { + case "": + req.VersionMatchV2 = resource.ResourceVersionMatchV2_Unset + case metav1.ResourceVersionMatchNotOlderThan: + req.VersionMatchV2 = resource.ResourceVersionMatchV2_NotOlderThan + case metav1.ResourceVersionMatchExact: + req.VersionMatchV2 = resource.ResourceVersionMatchV2_Exact + default: + return nil, predicate, apierrors.NewBadRequest( + fmt.Sprintf("unsupported version match: %v", opts.ResourceVersionMatch), + ) + } + if opts.Predicate.Label != nil && !opts.Predicate.Label.Empty() { requirements, selectable := opts.Predicate.Label.Requirements() if !selectable { @@ -91,38 +110,5 @@ func toListRequest(k *resource.ResourceKey, opts storage.ListOptions) (*resource } } - if opts.ResourceVersion != "" { - rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64) - if err != nil { - return nil, predicate, apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion)) - } - req.ResourceVersion = rv - } - - switch opts.ResourceVersionMatch { - case "", metav1.ResourceVersionMatchNotOlderThan: - req.VersionMatch = resource.ResourceVersionMatch_NotOlderThan - case metav1.ResourceVersionMatchExact: - req.VersionMatch = resource.ResourceVersionMatch_Exact - default: - return nil, predicate, apierrors.NewBadRequest( - fmt.Sprintf("unsupported version match: %v", opts.ResourceVersionMatch), - ) - } - return req, predicate, nil } - -func isUnchanged(codec runtime.Codec, obj runtime.Object, newObj runtime.Object) (bool, error) { - buf := new(bytes.Buffer) - if err := codec.Encode(obj, buf); err != nil { - return false, err - } - - newBuf := new(bytes.Buffer) - if err := codec.Encode(newObj, newBuf); err != nil { - return false, err - } - - return bytes.Equal(buf.Bytes(), newBuf.Bytes()), nil -} diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index c16fbc92ad3..9239b260deb 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -117,10 +117,31 @@ func newClient(opts options.StorageOptions, return nil, fmt.Errorf("expecting address for storage_type: %s", opts.StorageType) } - // Create a connection to the gRPC server. - conn, err := GrpcConn(opts.Address, reg) - if err != nil { - return nil, err + var ( + conn grpc.ClientConnInterface + err error + metrics = newClientMetrics(reg) + ) + // Create either a connection pool or a single connection. + // The connection pool __can__ be useful when connection to + // server side load balancers like kube-proxy. + if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageGrpcConnectionPool) { + conn, err = newPooledConn(&poolOpts{ + initialCapacity: 3, + maxCapacity: 6, + idleTimeout: time.Minute, + factory: func() (*grpc.ClientConn, error) { + return grpcConn(opts.Address, metrics) + }, + }) + if err != nil { + return nil, err + } + } else { + conn, err = grpcConn(opts.Address, metrics) + if err != nil { + return nil, err + } } // Create a client instance @@ -144,7 +165,7 @@ func newClient(opts options.StorageOptions, } } -func newResourceClient(conn *grpc.ClientConn, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (resource.ResourceClient, error) { +func newResourceClient(conn grpc.ClientConnInterface, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (resource.ResourceClient, error) { if !features.IsEnabledGlobally(featuremgmt.FlagAppPlatformGrpcClientAuth) { return resource.NewLegacyResourceClient(conn), nil } @@ -160,22 +181,8 @@ func newResourceClient(conn *grpc.ClientConn, cfg *setting.Cfg, features feature }) } -// GrpcConn creates a new gRPC connection to the provided address. -func GrpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, error) { - // This works for now as the Provide function is only called once during startup. - // We might eventually want to tight this factory to a struct for more runtime control. - metrics := clientMetrics{ - requestDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ - Name: "resource_server_client_request_duration_seconds", - Help: "Time spent executing requests to the resource server.", - Buckets: prometheus.ExponentialBuckets(0.008, 4, 7), - }, []string{"operation", "status_code"}), - requestRetries: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ - Name: "resource_server_client_request_retries_total", - Help: "Total number of retries for requests to the resource server.", - }, []string{"operation"}), - } - +// grpcConn creates a new gRPC connection to the provided address. +func grpcConn(address string, metrics *clientMetrics) (*grpc.ClientConn, error) { // Report gRPC status code errors as labels. unary, stream := instrument(metrics.requestDuration, middleware.ReportGRPCStatusOption) @@ -212,6 +219,12 @@ func GrpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, erro return grpc.NewClient(address, opts...) } +// GrpcConn is the public constructor that can be used for testing. +func GrpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, error) { + metrics := newClientMetrics(reg) + return grpcConn(address, metrics) +} + // instrument is the same as grpcclient.Instrument but without the middleware.ClientUserHeaderInterceptor // and middleware.StreamClientUserHeaderInterceptor as we don't need them. func instrument(requestDuration *prometheus.HistogramVec, instrumentationLabelOptions ...middleware.InstrumentationOption) ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) { @@ -223,3 +236,19 @@ func instrument(requestDuration *prometheus.HistogramVec, instrumentationLabelOp middleware.StreamClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...), } } + +func newClientMetrics(reg prometheus.Registerer) *clientMetrics { + // This works for now as the Provide function is only called once during startup. + // We might eventually want to tight this factory to a struct for more runtime control. + return &clientMetrics{ + requestDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Name: "resource_server_client_request_duration_seconds", + Help: "Time spent executing requests to the resource server.", + Buckets: prometheus.ExponentialBuckets(0.008, 4, 7), + }, []string{"operation", "status_code"}), + requestRetries: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Name: "resource_server_client_request_retries_total", + Help: "Total number of retries for requests to the resource server.", + }, []string{"operation"}), + } +} diff --git a/pkg/storage/unified/grpc_pool.go b/pkg/storage/unified/grpc_pool.go new file mode 100644 index 00000000000..cebc8abce86 --- /dev/null +++ b/pkg/storage/unified/grpc_pool.go @@ -0,0 +1,82 @@ +package unified + +import ( + "context" + "errors" + "fmt" + "time" + + grpcpool "github.com/1NCE-GmbH/grpc-go-pool" + "google.golang.org/grpc" +) + +// pooledClientConn implements grpc.ClientConnInterface using a connection from a pool. +type pooledClientConn struct { + pool *grpcpool.Pool + // For streaming we want to keep a single connection, as otherwise we saturate the pool. + // Streaming should only be used for watching. + streamConn grpc.ClientConnInterface +} + +// Invoke implements the grpc.ClientConnInterface.Invoke method. +func (pc *pooledClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { + conn, err := pc.pool.Get(ctx) + if err != nil { + return fmt.Errorf("failed to create grpc conn in pooled client: %w", err) + } + // Return connection to pool when done. + defer func() { + _ = conn.Close() + }() + return conn.ClientConn.Invoke(ctx, method, args, reply, opts...) +} + +// NewStream implements the grpc.ClientConnInterface.NewStream method. +func (pc *pooledClientConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + stream, err := pc.streamConn.NewStream(ctx, desc, method, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create grpc stream in pooled client: %w", err) + } + return stream, nil +} + +type poolOpts struct { + initialCapacity int + maxCapacity int + idleTimeout time.Duration + factory func() (*grpc.ClientConn, error) +} + +func (opts *poolOpts) validate() error { + if opts.initialCapacity <= 0 { + return errors.New("initial capacity is required") + } + if opts.maxCapacity < opts.initialCapacity { + return errors.New("max capacity is less than initial capacity") + } + if opts.idleTimeout <= 0 { + return errors.New("idle timeout is required") + } + if opts.factory == nil { + return errors.New("factory is required") + } + return nil +} + +func newPooledConn(opts *poolOpts) (grpc.ClientConnInterface, error) { + if err := opts.validate(); err != nil { + return nil, fmt.Errorf("failed to validate grpc connection pool options: %w", err) + } + pool, err := grpcpool.New(opts.factory, opts.initialCapacity, opts.maxCapacity, opts.idleTimeout) + if err != nil { + return nil, fmt.Errorf("failed to create grpc connection pool: %w", err) + } + streamConn, err := opts.factory() + if err != nil { + return nil, fmt.Errorf("failed to create groc streaming connection: %w", err) + } + return &pooledClientConn{ + pool: pool, + streamConn: streamConn, + }, nil +} diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index 649a0b7c9c0..c0eb700835d 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -14,6 +14,7 @@ import ( authnlib "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/tracing" @@ -40,7 +41,7 @@ type resourceClient struct { DiagnosticsClient } -func NewLegacyResourceClient(channel *grpc.ClientConn) ResourceClient { +func NewLegacyResourceClient(channel grpc.ClientConnInterface) ResourceClient { cc := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor) return &resourceClient{ ResourceStoreClient: NewResourceStoreClient(cc), @@ -99,7 +100,7 @@ type RemoteResourceClientConfig struct { AllowInsecure bool } -func NewRemoteResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg RemoteResourceClientConfig) (ResourceClient, error) { +func NewRemoteResourceClient(tracer tracing.Tracer, conn grpc.ClientConnInterface, cfg RemoteResourceClientConfig) (ResourceClient, error) { exchangeOpts := []authnlib.ExchangeClientOpts{} if cfg.AllowInsecure { diff --git a/pkg/storage/unified/resource/continue.go b/pkg/storage/unified/resource/continue.go index 70ca18a89ba..c8825ccde88 100644 --- a/pkg/storage/unified/resource/continue.go +++ b/pkg/storage/unified/resource/continue.go @@ -9,6 +9,7 @@ import ( type ContinueToken struct { StartOffset int64 `json:"o"` ResourceVersion int64 `json:"v"` + SortAscending bool `json:"s"` } func (c ContinueToken) String() string { diff --git a/pkg/storage/unified/resource/continue_test.go b/pkg/storage/unified/resource/continue_test.go index d82b74a4d85..3f67740efaf 100644 --- a/pkg/storage/unified/resource/continue_test.go +++ b/pkg/storage/unified/resource/continue_test.go @@ -10,6 +10,7 @@ func TestContinueToken(t *testing.T) { token := &ContinueToken{ ResourceVersion: 100, StartOffset: 50, + SortAscending: false, } - assert.Equal(t, "eyJvIjo1MCwidiI6MTAwfQ==", token.String()) + assert.Equal(t, "eyJvIjo1MCwidiI6MTAwLCJzIjpmYWxzZX0=", token.String()) } diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 3454e9471c8..68ff0c85fd1 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/storage/unified/resource -go 1.23.7 +go 1.24.1 replace ( github.com/grafana/grafana => ../../../.. @@ -15,9 +15,9 @@ require ( github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana-plugin-sdk-go v0.272.0 + github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 @@ -28,7 +28,7 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 gocloud.dev v0.40.0 golang.org/x/sync v0.11.0 - google.golang.org/grpc v1.70.0 + google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.5 k8s.io/apimachinery v0.32.1 ) @@ -52,15 +52,15 @@ require ( github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect - github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/aws/aws-sdk-go v1.55.5 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect @@ -86,19 +86,18 @@ require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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.11.0 // indirect - github.com/envoyproxy/go-control-plane v0.13.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.129.0 // indirect @@ -113,16 +112,16 @@ require ( github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -161,8 +160,8 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/magefile/mage v1.15.0 // indirect @@ -197,7 +196,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -224,12 +223,12 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect @@ -243,7 +242,7 @@ require ( golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect @@ -251,8 +250,8 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.220.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 11f1f5bcb10..cda7c667dbf 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -649,8 +649,9 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= @@ -697,8 +698,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= -github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= +github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= +github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -713,8 +714,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ= github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -777,7 +778,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -809,8 +809,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -881,8 +881,12 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= -github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= -github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= @@ -890,8 +894,8 @@ github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -977,8 +981,8 @@ github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuq github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -993,10 +997,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -1050,8 +1054,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -1163,8 +1167,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.272.0 h1:TmPIG+6e3lYGzkyfUfCHuaMaaiwDbkCacTZ7V/JaSeg= -github.com/grafana/grafana-plugin-sdk-go v0.272.0/go.mod h1:i/9KH9y/6m5hkRnG3H6aR2nOMPbJUmvo4XNrHjI15cU= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= +github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 h1:wdJy5x6M7auWDjUIubqhfZuZvphUMyjD7hxB3RqV4aE= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go.mod h1:jwYig4wlnLLq4HQKDpS95nDeZi4+DmcD17KYYS1gMJg= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -1272,11 +1276,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -1409,8 +1413,8 @@ github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1586,8 +1590,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= @@ -1596,8 +1600,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= @@ -1929,8 +1933,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -2271,10 +2275,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -2318,8 +2322,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 8f8d7925e13..69f2de4f1a9 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -24,19 +24,20 @@ const ( type ResourceVersionMatch int32 const ( - ResourceVersionMatch_NotOlderThan ResourceVersionMatch = 0 - ResourceVersionMatch_Exact ResourceVersionMatch = 1 + // Deprecated, use ResourceVersionMatch V2 + ResourceVersionMatch_DEPRECATED_NotOlderThan ResourceVersionMatch = 0 + ResourceVersionMatch_DEPRECATED_Exact ResourceVersionMatch = 1 ) // Enum value maps for ResourceVersionMatch. var ( ResourceVersionMatch_name = map[int32]string{ - 0: "NotOlderThan", - 1: "Exact", + 0: "DEPRECATED_NotOlderThan", + 1: "DEPRECATED_Exact", } ResourceVersionMatch_value = map[string]int32{ - "NotOlderThan": 0, - "Exact": 1, + "DEPRECATED_NotOlderThan": 0, + "DEPRECATED_Exact": 1, } ) @@ -67,6 +68,58 @@ func (ResourceVersionMatch) EnumDescriptor() ([]byte, []int) { return file_resource_proto_rawDescGZIP(), []int{0} } +type ResourceVersionMatchV2 int32 + +const ( + ResourceVersionMatchV2_UNKNOWN ResourceVersionMatchV2 = 0 + ResourceVersionMatchV2_Unset ResourceVersionMatchV2 = 1 + ResourceVersionMatchV2_Exact ResourceVersionMatchV2 = 2 + ResourceVersionMatchV2_NotOlderThan ResourceVersionMatchV2 = 3 +) + +// Enum value maps for ResourceVersionMatchV2. +var ( + ResourceVersionMatchV2_name = map[int32]string{ + 0: "UNKNOWN", + 1: "Unset", + 2: "Exact", + 3: "NotOlderThan", + } + ResourceVersionMatchV2_value = map[string]int32{ + "UNKNOWN": 0, + "Unset": 1, + "Exact": 2, + "NotOlderThan": 3, + } +) + +func (x ResourceVersionMatchV2) Enum() *ResourceVersionMatchV2 { + p := new(ResourceVersionMatchV2) + *p = x + return p +} + +func (x ResourceVersionMatchV2) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceVersionMatchV2) Descriptor() protoreflect.EnumDescriptor { + return file_resource_proto_enumTypes[1].Descriptor() +} + +func (ResourceVersionMatchV2) Type() protoreflect.EnumType { + return &file_resource_proto_enumTypes[1] +} + +func (x ResourceVersionMatchV2) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceVersionMatchV2.Descriptor instead. +func (ResourceVersionMatchV2) EnumDescriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{1} +} + type ListRequest_Source int32 const ( @@ -100,11 +153,11 @@ func (x ListRequest_Source) String() string { } func (ListRequest_Source) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[1].Descriptor() + return file_resource_proto_enumTypes[2].Descriptor() } func (ListRequest_Source) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[1] + return &file_resource_proto_enumTypes[2] } func (x ListRequest_Source) Number() protoreflect.EnumNumber { @@ -158,11 +211,11 @@ func (x WatchEvent_Type) String() string { } func (WatchEvent_Type) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[2].Descriptor() + return file_resource_proto_enumTypes[3].Descriptor() } func (WatchEvent_Type) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[2] + return &file_resource_proto_enumTypes[3] } func (x WatchEvent_Type) Number() protoreflect.EnumNumber { @@ -212,11 +265,11 @@ func (x BulkRequest_Action) String() string { } func (BulkRequest_Action) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[3].Descriptor() + return file_resource_proto_enumTypes[4].Descriptor() } func (BulkRequest_Action) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[3] + return &file_resource_proto_enumTypes[4] } func (x BulkRequest_Action) Number() protoreflect.EnumNumber { @@ -264,11 +317,11 @@ func (x HealthCheckResponse_ServingStatus) String() string { } func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[4].Descriptor() + return file_resource_proto_enumTypes[5].Descriptor() } func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[4] + return &file_resource_proto_enumTypes[5] } func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { @@ -339,11 +392,11 @@ func (x ResourceTableColumnDefinition_ColumnType) String() string { } func (ResourceTableColumnDefinition_ColumnType) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[5].Descriptor() + return file_resource_proto_enumTypes[6].Descriptor() } func (ResourceTableColumnDefinition_ColumnType) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[5] + return &file_resource_proto_enumTypes[6] } func (x ResourceTableColumnDefinition_ColumnType) Number() protoreflect.EnumNumber { @@ -387,11 +440,11 @@ func (x PutBlobRequest_Method) String() string { } func (PutBlobRequest_Method) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[6].Descriptor() + return file_resource_proto_enumTypes[7].Descriptor() } func (PutBlobRequest_Method) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[6] + return &file_resource_proto_enumTypes[7] } func (x PutBlobRequest_Method) Number() protoreflect.EnumNumber { @@ -400,7 +453,7 @@ func (x PutBlobRequest_Method) Number() protoreflect.EnumNumber { // Deprecated: Use PutBlobRequest_Method.Descriptor instead. func (PutBlobRequest_Method) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{36, 0} + return file_resource_proto_rawDescGZIP(), []int{34, 0} } type ResourceKey struct { @@ -1152,10 +1205,8 @@ type ReadRequest struct { Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Optionally pick an explicit resource version ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` - // Optionally decide to return the latest RV if deleted - IncludeDeleted bool `protobuf:"varint,3,opt,name=include_deleted,json=includeDeleted,proto3" json:"include_deleted,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadRequest) Reset() { @@ -1202,13 +1253,6 @@ func (x *ReadRequest) GetResourceVersion() int64 { return 0 } -func (x *ReadRequest) GetIncludeDeleted() bool { - if x != nil { - return x.IncludeDeleted - } - return false -} - type ReadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -1408,16 +1452,18 @@ type ListRequest struct { // The resource version ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` // List options - VersionMatch ResourceVersionMatch `protobuf:"varint,3,opt,name=version_match,json=versionMatch,proto3,enum=resource.ResourceVersionMatch" json:"version_match,omitempty"` + // DEPRECATED - use version_match_v2 + VersionMatch *ResourceVersionMatch `protobuf:"varint,3,opt,name=version_match,json=versionMatch,proto3,enum=resource.ResourceVersionMatch,oneof" json:"version_match,omitempty"` // Maximum number of items to return // NOTE responses will also be limited by the response payload size Limit int64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` // Filtering Options *ListOptions `protobuf:"bytes,5,opt,name=options,proto3" json:"options,omitempty"` // Select values from history or trash - Source ListRequest_Source `protobuf:"varint,6,opt,name=source,proto3,enum=resource.ListRequest_Source" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Source ListRequest_Source `protobuf:"varint,6,opt,name=source,proto3,enum=resource.ListRequest_Source" json:"source,omitempty"` + VersionMatchV2 ResourceVersionMatchV2 `protobuf:"varint,7,opt,name=version_match_v2,json=versionMatchV2,proto3,enum=resource.ResourceVersionMatchV2" json:"version_match_v2,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListRequest) Reset() { @@ -1465,10 +1511,10 @@ func (x *ListRequest) GetResourceVersion() int64 { } func (x *ListRequest) GetVersionMatch() ResourceVersionMatch { - if x != nil { - return x.VersionMatch + if x != nil && x.VersionMatch != nil { + return *x.VersionMatch } - return ResourceVersionMatch_NotOlderThan + return ResourceVersionMatch_DEPRECATED_NotOlderThan } func (x *ListRequest) GetLimit() int64 { @@ -1492,6 +1538,13 @@ func (x *ListRequest) GetSource() ListRequest_Source { return ListRequest_STORE } +func (x *ListRequest) GetVersionMatchV2() ResourceVersionMatchV2 { + if x != nil { + return x.VersionMatchV2 + } + return ResourceVersionMatchV2_UNKNOWN +} + type ListResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Items []*ResourceWrapper `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` @@ -2844,114 +2897,6 @@ func (x *ResourceTableRow) GetObject() []byte { return nil } -type RestoreRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Full key must be set - Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - // The resource version to restore - ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RestoreRequest) Reset() { - *x = RestoreRequest{} - mi := &file_resource_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RestoreRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RestoreRequest) ProtoMessage() {} - -func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[34] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. -func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{34} -} - -func (x *RestoreRequest) GetKey() *ResourceKey { - if x != nil { - return x.Key - } - return nil -} - -func (x *RestoreRequest) GetResourceVersion() int64 { - if x != nil { - return x.ResourceVersion - } - return 0 -} - -type RestoreResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Error details - Error *ErrorResult `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` - // The updated resource version - ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RestoreResponse) Reset() { - *x = RestoreResponse{} - mi := &file_resource_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RestoreResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RestoreResponse) ProtoMessage() {} - -func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[35] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. -func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{35} -} - -func (x *RestoreResponse) GetError() *ErrorResult { - if x != nil { - return x.Error - } - return nil -} - -func (x *RestoreResponse) GetResourceVersion() int64 { - if x != nil { - return x.ResourceVersion - } - return 0 -} - type PutBlobRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource that will use this blob @@ -2970,7 +2915,7 @@ type PutBlobRequest struct { func (x *PutBlobRequest) Reset() { *x = PutBlobRequest{} - mi := &file_resource_proto_msgTypes[36] + mi := &file_resource_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2982,7 +2927,7 @@ func (x *PutBlobRequest) String() string { func (*PutBlobRequest) ProtoMessage() {} func (x *PutBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[36] + mi := &file_resource_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2995,7 +2940,7 @@ func (x *PutBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PutBlobRequest.ProtoReflect.Descriptor instead. func (*PutBlobRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{36} + return file_resource_proto_rawDescGZIP(), []int{34} } func (x *PutBlobRequest) GetResource() *ResourceKey { @@ -3048,7 +2993,7 @@ type PutBlobResponse struct { func (x *PutBlobResponse) Reset() { *x = PutBlobResponse{} - mi := &file_resource_proto_msgTypes[37] + mi := &file_resource_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3060,7 +3005,7 @@ func (x *PutBlobResponse) String() string { func (*PutBlobResponse) ProtoMessage() {} func (x *PutBlobResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[37] + mi := &file_resource_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3073,7 +3018,7 @@ func (x *PutBlobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PutBlobResponse.ProtoReflect.Descriptor instead. func (*PutBlobResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{37} + return file_resource_proto_rawDescGZIP(), []int{35} } func (x *PutBlobResponse) GetError() *ErrorResult { @@ -3140,7 +3085,7 @@ type GetBlobRequest struct { func (x *GetBlobRequest) Reset() { *x = GetBlobRequest{} - mi := &file_resource_proto_msgTypes[38] + mi := &file_resource_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3152,7 +3097,7 @@ func (x *GetBlobRequest) String() string { func (*GetBlobRequest) ProtoMessage() {} func (x *GetBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[38] + mi := &file_resource_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3165,7 +3110,7 @@ func (x *GetBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBlobRequest.ProtoReflect.Descriptor instead. func (*GetBlobRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{38} + return file_resource_proto_rawDescGZIP(), []int{36} } func (x *GetBlobRequest) GetResource() *ResourceKey { @@ -3214,7 +3159,7 @@ type GetBlobResponse struct { func (x *GetBlobResponse) Reset() { *x = GetBlobResponse{} - mi := &file_resource_proto_msgTypes[39] + mi := &file_resource_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3226,7 +3171,7 @@ func (x *GetBlobResponse) String() string { func (*GetBlobResponse) ProtoMessage() {} func (x *GetBlobResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[39] + mi := &file_resource_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3239,7 +3184,7 @@ func (x *GetBlobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBlobResponse.ProtoReflect.Descriptor instead. func (*GetBlobResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{39} + return file_resource_proto_rawDescGZIP(), []int{37} } func (x *GetBlobResponse) GetError() *ErrorResult { @@ -3280,7 +3225,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} - mi := &file_resource_proto_msgTypes[40] + mi := &file_resource_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3292,7 +3237,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[40] + mi := &file_resource_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3339,7 +3284,7 @@ type BulkResponse_Summary struct { func (x *BulkResponse_Summary) Reset() { *x = BulkResponse_Summary{} - mi := &file_resource_proto_msgTypes[41] + mi := &file_resource_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3351,7 +3296,7 @@ func (x *BulkResponse_Summary) String() string { func (*BulkResponse_Summary) ProtoMessage() {} func (x *BulkResponse_Summary) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[41] + mi := &file_resource_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3435,7 +3380,7 @@ type BulkResponse_Rejected struct { func (x *BulkResponse_Rejected) Reset() { *x = BulkResponse_Rejected{} - mi := &file_resource_proto_msgTypes[42] + mi := &file_resource_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3447,7 +3392,7 @@ func (x *BulkResponse_Rejected) String() string { func (*BulkResponse_Rejected) ProtoMessage() {} func (x *BulkResponse_Rejected) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[42] + mi := &file_resource_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3498,7 +3443,7 @@ type ResourceStatsResponse_Stats struct { func (x *ResourceStatsResponse_Stats) Reset() { *x = ResourceStatsResponse_Stats{} - mi := &file_resource_proto_msgTypes[43] + mi := &file_resource_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3510,7 +3455,7 @@ func (x *ResourceStatsResponse_Stats) String() string { func (*ResourceStatsResponse_Stats) ProtoMessage() {} func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[43] + mi := &file_resource_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3557,7 +3502,7 @@ type ResourceSearchRequest_Sort struct { func (x *ResourceSearchRequest_Sort) Reset() { *x = ResourceSearchRequest_Sort{} - mi := &file_resource_proto_msgTypes[44] + mi := &file_resource_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3569,7 +3514,7 @@ func (x *ResourceSearchRequest_Sort) String() string { func (*ResourceSearchRequest_Sort) ProtoMessage() {} func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[44] + mi := &file_resource_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3609,7 +3554,7 @@ type ResourceSearchRequest_Facet struct { func (x *ResourceSearchRequest_Facet) Reset() { *x = ResourceSearchRequest_Facet{} - mi := &file_resource_proto_msgTypes[45] + mi := &file_resource_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3621,7 +3566,7 @@ func (x *ResourceSearchRequest_Facet) String() string { func (*ResourceSearchRequest_Facet) ProtoMessage() {} func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[45] + mi := &file_resource_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3666,7 +3611,7 @@ type ResourceSearchResponse_Facet struct { func (x *ResourceSearchResponse_Facet) Reset() { *x = ResourceSearchResponse_Facet{} - mi := &file_resource_proto_msgTypes[47] + mi := &file_resource_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3678,7 +3623,7 @@ func (x *ResourceSearchResponse_Facet) String() string { func (*ResourceSearchResponse_Facet) ProtoMessage() {} func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[47] + mi := &file_resource_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3732,7 +3677,7 @@ type ResourceSearchResponse_TermFacet struct { func (x *ResourceSearchResponse_TermFacet) Reset() { *x = ResourceSearchResponse_TermFacet{} - mi := &file_resource_proto_msgTypes[48] + mi := &file_resource_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3744,7 +3689,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string { func (*ResourceSearchResponse_TermFacet) ProtoMessage() {} func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[48] + mi := &file_resource_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3794,7 +3739,7 @@ type ListManagedObjectsResponse_Item struct { func (x *ListManagedObjectsResponse_Item) Reset() { *x = ListManagedObjectsResponse_Item{} - mi := &file_resource_proto_msgTypes[50] + mi := &file_resource_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3806,7 +3751,7 @@ func (x *ListManagedObjectsResponse_Item) String() string { func (*ListManagedObjectsResponse_Item) ProtoMessage() {} func (x *ListManagedObjectsResponse_Item) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[50] + mi := &file_resource_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3877,7 +3822,7 @@ type CountManagedObjectsResponse_ResourceCount struct { func (x *CountManagedObjectsResponse_ResourceCount) Reset() { *x = CountManagedObjectsResponse_ResourceCount{} - mi := &file_resource_proto_msgTypes[51] + mi := &file_resource_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3889,7 +3834,7 @@ func (x *CountManagedObjectsResponse_ResourceCount) String() string { func (*CountManagedObjectsResponse_ResourceCount) ProtoMessage() {} func (x *CountManagedObjectsResponse_ResourceCount) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[51] + mi := &file_resource_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3962,7 +3907,7 @@ type ResourceTableColumnDefinition_Properties struct { func (x *ResourceTableColumnDefinition_Properties) Reset() { *x = ResourceTableColumnDefinition_Properties{} - mi := &file_resource_proto_msgTypes[52] + mi := &file_resource_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3974,7 +3919,7 @@ func (x *ResourceTableColumnDefinition_Properties) String() string { func (*ResourceTableColumnDefinition_Properties) ProtoMessage() {} func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[52] + mi := &file_resource_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4107,529 +4052,522 @@ var file_resource_proto_rawDesc = string([]byte{ 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x8a, 0x01, 0x0a, - 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, - 0x64, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x7c, 0x0a, 0x0c, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x66, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x22, 0xcf, 0x02, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, - 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x0d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x61, 0x0a, 0x0b, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, + 0x7c, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, + 0x0b, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, + 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0xb2, 0x03, 0x0a, 0x0b, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, + 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x0d, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x48, 0x00, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2f, 0x0a, 0x07, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x10, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x5f, 0x76, 0x32, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x0c, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x2b, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, - 0x07, 0x48, 0x49, 0x53, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x53, 0x48, 0x10, 0x02, 0x22, 0xf1, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, - 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, - 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, - 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, - 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xb9, 0x01, 0x0a, 0x0c, 0x57, 0x61, - 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, - 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, - 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, - 0x6c, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, - 0x73, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, - 0x5f, 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, - 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x22, 0xdf, 0x02, 0x0a, 0x0a, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, - 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x08, - 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x70, - 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x1a, 0x3a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x22, 0x52, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, - 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, - 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0xd7, 0x01, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x34, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, - 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, - 0x03, 0x22, 0xda, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, - 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x38, 0x0a, - 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x07, - 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x3b, 0x0a, 0x08, 0x72, 0x65, 0x6a, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x52, 0x08, 0x72, 0x65, 0x6a, 0x65, - 0x63, 0x74, 0x65, 0x64, 0x1a, 0x86, 0x02, 0x0a, 0x07, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, - 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x70, - 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x68, - 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x72, - 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x7f, 0x0a, - 0x08, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, - 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x62, - 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6b, 0x69, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x69, 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x22, 0xd2, 0x01, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a, 0x4f, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8e, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x66, 0x65, - 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, - 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x73, - 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x72, - 0x74, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x40, 0x0a, 0x05, 0x66, 0x61, 0x63, - 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, - 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, - 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, - 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, - 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, - 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, - 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, - 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, - 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, - 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, - 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, - 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, - 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, - 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x85, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, - 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd4, 0x02, - 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x05, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32, 0x52, + 0x0e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32, 0x22, + 0x2b, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x4f, + 0x52, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x49, 0x53, 0x54, 0x4f, 0x52, 0x59, 0x10, + 0x01, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x53, 0x48, 0x10, 0x02, 0x42, 0x10, 0x0a, 0x0e, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x22, 0xf1, + 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, - 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, - 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x22, 0x5e, 0x0a, 0x1a, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, - 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x7b, 0x0a, 0x0d, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, - 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, - 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, - 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, - 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x07, 0x63, 0x6f, 0x6c, - 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, - 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, - 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, - 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, - 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, - 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0a, - 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, - 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, - 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x1a, 0xae, 0x01, 0x0a, - 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x75, - 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, - 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, - 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x95, 0x01, - 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, - 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, - 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, - 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x10, 0x04, 0x12, 0x09, 0x0a, - 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, - 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, 0x10, 0x07, 0x12, 0x0d, - 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x08, 0x12, 0x0a, 0x0a, - 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, - 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, - 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, - 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x64, 0x0a, 0x0e, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, + 0x6f, 0x72, 0x22, 0xb9, 0x01, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, + 0x74, 0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, + 0x72, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x22, 0xdf, + 0x02, 0x0a, 0x0a, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2d, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x1a, 0x3a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x52, 0x0a, 0x04, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, + 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, + 0x41, 0x52, 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, + 0x22, 0xd7, 0x01, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b, 0x0a, + 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, + 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x22, 0xda, 0x04, 0x0a, 0x0c, 0x42, + 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, + 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x38, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, + 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x12, 0x3b, 0x0a, 0x08, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, + 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x52, 0x08, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x86, 0x02, + 0x0a, 0x07, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x72, + 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x48, + 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x7f, 0x0a, 0x08, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x62, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x6b, 0x69, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x69, + 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xd2, 0x01, 0x0a, 0x15, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xd3, 0x01, - 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, - 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, - 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, - 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0xaa, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, - 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x75, 0x73, 0x74, - 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x75, 0x69, 0x64, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, - 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, - 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, - 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, - 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x74, 0x6f, - 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, - 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, - 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x28, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, + 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a, + 0x4f, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x8e, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x66, + 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, + 0x42, 0x79, 0x12, 0x40, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, + 0x61, 0x63, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, + 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, + 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, + 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, + 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, + 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xd9, - 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, - 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, - 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, + 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, + 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, + 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, + 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, + 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x85, + 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, + 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd4, 0x02, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, - 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, - 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, - 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, - 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, - 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, + 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x5e, 0x0a, + 0x1a, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, + 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x7b, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, + 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, + 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, + 0x22, 0x87, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, + 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, + 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, + 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, + 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, + 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x46, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, + 0x72, 0x72, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, + 0x72, 0x61, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, + 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, + 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, + 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, + 0x6f, 0x72, 0x69, 0x74, 0x79, 0x1a, 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, + 0x74, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, + 0x71, 0x75, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, + 0x65, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, + 0x65, 0x65, 0x54, 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, + 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, + 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, + 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, + 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, + 0x4e, 0x54, 0x36, 0x34, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, + 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, + 0x04, 0x44, 0x41, 0x54, 0x45, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, + 0x54, 0x49, 0x4d, 0x45, 0x10, 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, + 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, + 0x01, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x52, 0x6f, 0x77, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, + 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, + 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, + 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, + 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, + 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, + 0xaa, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x89, 0x01, 0x0a, + 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, + 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x49, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, + 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x4e, + 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x14, 0x0a, + 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x45, 0x78, 0x61, 0x63, + 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x6e, + 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x02, + 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, + 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, + 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, + 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x15, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x32, + 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, + 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xd9, 0x01, 0x0a, 0x12, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, + 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, + 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, + 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, + 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, + 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, + 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, + 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, + 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, }) var ( @@ -4644,166 +4582,162 @@ func file_resource_proto_rawDescGZIP() []byte { return file_resource_proto_rawDescData } -var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 51) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch - (ListRequest_Source)(0), // 1: resource.ListRequest.Source - (WatchEvent_Type)(0), // 2: resource.WatchEvent.Type - (BulkRequest_Action)(0), // 3: resource.BulkRequest.Action - (HealthCheckResponse_ServingStatus)(0), // 4: resource.HealthCheckResponse.ServingStatus - (ResourceTableColumnDefinition_ColumnType)(0), // 5: resource.ResourceTableColumnDefinition.ColumnType - (PutBlobRequest_Method)(0), // 6: resource.PutBlobRequest.Method - (*ResourceKey)(nil), // 7: resource.ResourceKey - (*ResourceWrapper)(nil), // 8: resource.ResourceWrapper - (*ErrorResult)(nil), // 9: resource.ErrorResult - (*ErrorDetails)(nil), // 10: resource.ErrorDetails - (*ErrorCause)(nil), // 11: resource.ErrorCause - (*CreateRequest)(nil), // 12: resource.CreateRequest - (*CreateResponse)(nil), // 13: resource.CreateResponse - (*UpdateRequest)(nil), // 14: resource.UpdateRequest - (*UpdateResponse)(nil), // 15: resource.UpdateResponse - (*DeleteRequest)(nil), // 16: resource.DeleteRequest - (*DeleteResponse)(nil), // 17: resource.DeleteResponse - (*ReadRequest)(nil), // 18: resource.ReadRequest - (*ReadResponse)(nil), // 19: resource.ReadResponse - (*Requirement)(nil), // 20: resource.Requirement - (*ListOptions)(nil), // 21: resource.ListOptions - (*ListRequest)(nil), // 22: resource.ListRequest - (*ListResponse)(nil), // 23: resource.ListResponse - (*WatchRequest)(nil), // 24: resource.WatchRequest - (*WatchEvent)(nil), // 25: resource.WatchEvent - (*BulkRequest)(nil), // 26: resource.BulkRequest - (*BulkResponse)(nil), // 27: resource.BulkResponse - (*ResourceStatsRequest)(nil), // 28: resource.ResourceStatsRequest - (*ResourceStatsResponse)(nil), // 29: resource.ResourceStatsResponse - (*ResourceSearchRequest)(nil), // 30: resource.ResourceSearchRequest - (*ResourceSearchResponse)(nil), // 31: resource.ResourceSearchResponse - (*ListManagedObjectsRequest)(nil), // 32: resource.ListManagedObjectsRequest - (*ListManagedObjectsResponse)(nil), // 33: resource.ListManagedObjectsResponse - (*CountManagedObjectsRequest)(nil), // 34: resource.CountManagedObjectsRequest - (*CountManagedObjectsResponse)(nil), // 35: resource.CountManagedObjectsResponse - (*HealthCheckRequest)(nil), // 36: resource.HealthCheckRequest - (*HealthCheckResponse)(nil), // 37: resource.HealthCheckResponse - (*ResourceTable)(nil), // 38: resource.ResourceTable - (*ResourceTableColumnDefinition)(nil), // 39: resource.ResourceTableColumnDefinition - (*ResourceTableRow)(nil), // 40: resource.ResourceTableRow - (*RestoreRequest)(nil), // 41: resource.RestoreRequest - (*RestoreResponse)(nil), // 42: resource.RestoreResponse - (*PutBlobRequest)(nil), // 43: resource.PutBlobRequest - (*PutBlobResponse)(nil), // 44: resource.PutBlobResponse - (*GetBlobRequest)(nil), // 45: resource.GetBlobRequest - (*GetBlobResponse)(nil), // 46: resource.GetBlobResponse - (*WatchEvent_Resource)(nil), // 47: resource.WatchEvent.Resource - (*BulkResponse_Summary)(nil), // 48: resource.BulkResponse.Summary - (*BulkResponse_Rejected)(nil), // 49: resource.BulkResponse.Rejected - (*ResourceStatsResponse_Stats)(nil), // 50: resource.ResourceStatsResponse.Stats - (*ResourceSearchRequest_Sort)(nil), // 51: resource.ResourceSearchRequest.Sort - (*ResourceSearchRequest_Facet)(nil), // 52: resource.ResourceSearchRequest.Facet - nil, // 53: resource.ResourceSearchRequest.FacetEntry - (*ResourceSearchResponse_Facet)(nil), // 54: resource.ResourceSearchResponse.Facet - (*ResourceSearchResponse_TermFacet)(nil), // 55: resource.ResourceSearchResponse.TermFacet - nil, // 56: resource.ResourceSearchResponse.FacetEntry - (*ListManagedObjectsResponse_Item)(nil), // 57: resource.ListManagedObjectsResponse.Item - (*CountManagedObjectsResponse_ResourceCount)(nil), // 58: resource.CountManagedObjectsResponse.ResourceCount - (*ResourceTableColumnDefinition_Properties)(nil), // 59: resource.ResourceTableColumnDefinition.Properties + (ResourceVersionMatchV2)(0), // 1: resource.ResourceVersionMatchV2 + (ListRequest_Source)(0), // 2: resource.ListRequest.Source + (WatchEvent_Type)(0), // 3: resource.WatchEvent.Type + (BulkRequest_Action)(0), // 4: resource.BulkRequest.Action + (HealthCheckResponse_ServingStatus)(0), // 5: resource.HealthCheckResponse.ServingStatus + (ResourceTableColumnDefinition_ColumnType)(0), // 6: resource.ResourceTableColumnDefinition.ColumnType + (PutBlobRequest_Method)(0), // 7: resource.PutBlobRequest.Method + (*ResourceKey)(nil), // 8: resource.ResourceKey + (*ResourceWrapper)(nil), // 9: resource.ResourceWrapper + (*ErrorResult)(nil), // 10: resource.ErrorResult + (*ErrorDetails)(nil), // 11: resource.ErrorDetails + (*ErrorCause)(nil), // 12: resource.ErrorCause + (*CreateRequest)(nil), // 13: resource.CreateRequest + (*CreateResponse)(nil), // 14: resource.CreateResponse + (*UpdateRequest)(nil), // 15: resource.UpdateRequest + (*UpdateResponse)(nil), // 16: resource.UpdateResponse + (*DeleteRequest)(nil), // 17: resource.DeleteRequest + (*DeleteResponse)(nil), // 18: resource.DeleteResponse + (*ReadRequest)(nil), // 19: resource.ReadRequest + (*ReadResponse)(nil), // 20: resource.ReadResponse + (*Requirement)(nil), // 21: resource.Requirement + (*ListOptions)(nil), // 22: resource.ListOptions + (*ListRequest)(nil), // 23: resource.ListRequest + (*ListResponse)(nil), // 24: resource.ListResponse + (*WatchRequest)(nil), // 25: resource.WatchRequest + (*WatchEvent)(nil), // 26: resource.WatchEvent + (*BulkRequest)(nil), // 27: resource.BulkRequest + (*BulkResponse)(nil), // 28: resource.BulkResponse + (*ResourceStatsRequest)(nil), // 29: resource.ResourceStatsRequest + (*ResourceStatsResponse)(nil), // 30: resource.ResourceStatsResponse + (*ResourceSearchRequest)(nil), // 31: resource.ResourceSearchRequest + (*ResourceSearchResponse)(nil), // 32: resource.ResourceSearchResponse + (*ListManagedObjectsRequest)(nil), // 33: resource.ListManagedObjectsRequest + (*ListManagedObjectsResponse)(nil), // 34: resource.ListManagedObjectsResponse + (*CountManagedObjectsRequest)(nil), // 35: resource.CountManagedObjectsRequest + (*CountManagedObjectsResponse)(nil), // 36: resource.CountManagedObjectsResponse + (*HealthCheckRequest)(nil), // 37: resource.HealthCheckRequest + (*HealthCheckResponse)(nil), // 38: resource.HealthCheckResponse + (*ResourceTable)(nil), // 39: resource.ResourceTable + (*ResourceTableColumnDefinition)(nil), // 40: resource.ResourceTableColumnDefinition + (*ResourceTableRow)(nil), // 41: resource.ResourceTableRow + (*PutBlobRequest)(nil), // 42: resource.PutBlobRequest + (*PutBlobResponse)(nil), // 43: resource.PutBlobResponse + (*GetBlobRequest)(nil), // 44: resource.GetBlobRequest + (*GetBlobResponse)(nil), // 45: resource.GetBlobResponse + (*WatchEvent_Resource)(nil), // 46: resource.WatchEvent.Resource + (*BulkResponse_Summary)(nil), // 47: resource.BulkResponse.Summary + (*BulkResponse_Rejected)(nil), // 48: resource.BulkResponse.Rejected + (*ResourceStatsResponse_Stats)(nil), // 49: resource.ResourceStatsResponse.Stats + (*ResourceSearchRequest_Sort)(nil), // 50: resource.ResourceSearchRequest.Sort + (*ResourceSearchRequest_Facet)(nil), // 51: resource.ResourceSearchRequest.Facet + nil, // 52: resource.ResourceSearchRequest.FacetEntry + (*ResourceSearchResponse_Facet)(nil), // 53: resource.ResourceSearchResponse.Facet + (*ResourceSearchResponse_TermFacet)(nil), // 54: resource.ResourceSearchResponse.TermFacet + nil, // 55: resource.ResourceSearchResponse.FacetEntry + (*ListManagedObjectsResponse_Item)(nil), // 56: resource.ListManagedObjectsResponse.Item + (*CountManagedObjectsResponse_ResourceCount)(nil), // 57: resource.CountManagedObjectsResponse.ResourceCount + (*ResourceTableColumnDefinition_Properties)(nil), // 58: resource.ResourceTableColumnDefinition.Properties } var file_resource_proto_depIdxs = []int32{ - 10, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails - 11, // 1: resource.ErrorDetails.causes:type_name -> resource.ErrorCause - 7, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey - 9, // 3: resource.CreateResponse.error:type_name -> resource.ErrorResult - 7, // 4: resource.UpdateRequest.key:type_name -> resource.ResourceKey - 9, // 5: resource.UpdateResponse.error:type_name -> resource.ErrorResult - 7, // 6: resource.DeleteRequest.key:type_name -> resource.ResourceKey - 9, // 7: resource.DeleteResponse.error:type_name -> resource.ErrorResult - 7, // 8: resource.ReadRequest.key:type_name -> resource.ResourceKey - 9, // 9: resource.ReadResponse.error:type_name -> resource.ErrorResult - 7, // 10: resource.ListOptions.key:type_name -> resource.ResourceKey - 20, // 11: resource.ListOptions.labels:type_name -> resource.Requirement - 20, // 12: resource.ListOptions.fields:type_name -> resource.Requirement + 11, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails + 12, // 1: resource.ErrorDetails.causes:type_name -> resource.ErrorCause + 8, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey + 10, // 3: resource.CreateResponse.error:type_name -> resource.ErrorResult + 8, // 4: resource.UpdateRequest.key:type_name -> resource.ResourceKey + 10, // 5: resource.UpdateResponse.error:type_name -> resource.ErrorResult + 8, // 6: resource.DeleteRequest.key:type_name -> resource.ResourceKey + 10, // 7: resource.DeleteResponse.error:type_name -> resource.ErrorResult + 8, // 8: resource.ReadRequest.key:type_name -> resource.ResourceKey + 10, // 9: resource.ReadResponse.error:type_name -> resource.ErrorResult + 8, // 10: resource.ListOptions.key:type_name -> resource.ResourceKey + 21, // 11: resource.ListOptions.labels:type_name -> resource.Requirement + 21, // 12: resource.ListOptions.fields:type_name -> resource.Requirement 0, // 13: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch - 21, // 14: resource.ListRequest.options:type_name -> resource.ListOptions - 1, // 15: resource.ListRequest.source:type_name -> resource.ListRequest.Source - 8, // 16: resource.ListResponse.items:type_name -> resource.ResourceWrapper - 9, // 17: resource.ListResponse.error:type_name -> resource.ErrorResult - 21, // 18: resource.WatchRequest.options:type_name -> resource.ListOptions - 2, // 19: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 47, // 20: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 47, // 21: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource - 7, // 22: resource.BulkRequest.key:type_name -> resource.ResourceKey - 3, // 23: resource.BulkRequest.action:type_name -> resource.BulkRequest.Action - 9, // 24: resource.BulkResponse.error:type_name -> resource.ErrorResult - 48, // 25: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary - 49, // 26: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected - 9, // 27: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult - 50, // 28: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats - 21, // 29: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions - 7, // 30: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey - 51, // 31: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort - 53, // 32: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry - 9, // 33: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult - 7, // 34: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey - 38, // 35: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable - 56, // 36: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry - 57, // 37: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item - 9, // 38: resource.ListManagedObjectsResponse.error:type_name -> resource.ErrorResult - 58, // 39: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount - 9, // 40: resource.CountManagedObjectsResponse.error:type_name -> resource.ErrorResult - 4, // 41: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 39, // 42: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition - 40, // 43: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow - 5, // 44: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType - 59, // 45: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties - 7, // 46: resource.ResourceTableRow.key:type_name -> resource.ResourceKey - 7, // 47: resource.RestoreRequest.key:type_name -> resource.ResourceKey - 9, // 48: resource.RestoreResponse.error:type_name -> resource.ErrorResult - 7, // 49: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey - 6, // 50: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method - 9, // 51: resource.PutBlobResponse.error:type_name -> resource.ErrorResult - 7, // 52: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey - 9, // 53: resource.GetBlobResponse.error:type_name -> resource.ErrorResult - 7, // 54: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey - 3, // 55: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action - 52, // 56: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet - 55, // 57: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet - 54, // 58: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet - 7, // 59: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey - 18, // 60: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 12, // 61: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 14, // 62: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 16, // 63: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 41, // 64: resource.ResourceStore.Restore:input_type -> resource.RestoreRequest - 22, // 65: resource.ResourceStore.List:input_type -> resource.ListRequest - 24, // 66: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 26, // 67: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest - 30, // 68: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest - 28, // 69: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest - 34, // 70: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest - 32, // 71: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest - 43, // 72: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest - 45, // 73: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest - 36, // 74: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 19, // 75: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 13, // 76: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 15, // 77: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 17, // 78: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 42, // 79: resource.ResourceStore.Restore:output_type -> resource.RestoreResponse - 23, // 80: resource.ResourceStore.List:output_type -> resource.ListResponse - 25, // 81: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 27, // 82: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse - 31, // 83: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse - 29, // 84: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse - 35, // 85: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse - 33, // 86: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse - 44, // 87: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse - 46, // 88: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse - 37, // 89: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 75, // [75:90] is the sub-list for method output_type - 60, // [60:75] is the sub-list for method input_type - 60, // [60:60] is the sub-list for extension type_name - 60, // [60:60] is the sub-list for extension extendee - 0, // [0:60] is the sub-list for field type_name + 22, // 14: resource.ListRequest.options:type_name -> resource.ListOptions + 2, // 15: resource.ListRequest.source:type_name -> resource.ListRequest.Source + 1, // 16: resource.ListRequest.version_match_v2:type_name -> resource.ResourceVersionMatchV2 + 9, // 17: resource.ListResponse.items:type_name -> resource.ResourceWrapper + 10, // 18: resource.ListResponse.error:type_name -> resource.ErrorResult + 22, // 19: resource.WatchRequest.options:type_name -> resource.ListOptions + 3, // 20: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type + 46, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 46, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 8, // 23: resource.BulkRequest.key:type_name -> resource.ResourceKey + 4, // 24: resource.BulkRequest.action:type_name -> resource.BulkRequest.Action + 10, // 25: resource.BulkResponse.error:type_name -> resource.ErrorResult + 47, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary + 48, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected + 10, // 28: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult + 49, // 29: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats + 22, // 30: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions + 8, // 31: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey + 50, // 32: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort + 52, // 33: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry + 10, // 34: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult + 8, // 35: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey + 39, // 36: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable + 55, // 37: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry + 56, // 38: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item + 10, // 39: resource.ListManagedObjectsResponse.error:type_name -> resource.ErrorResult + 57, // 40: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount + 10, // 41: resource.CountManagedObjectsResponse.error:type_name -> resource.ErrorResult + 5, // 42: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus + 40, // 43: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition + 41, // 44: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow + 6, // 45: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType + 58, // 46: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties + 8, // 47: resource.ResourceTableRow.key:type_name -> resource.ResourceKey + 8, // 48: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey + 7, // 49: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method + 10, // 50: resource.PutBlobResponse.error:type_name -> resource.ErrorResult + 8, // 51: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey + 10, // 52: resource.GetBlobResponse.error:type_name -> resource.ErrorResult + 8, // 53: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey + 4, // 54: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action + 51, // 55: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet + 54, // 56: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet + 53, // 57: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet + 8, // 58: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey + 19, // 59: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 13, // 60: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 15, // 61: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 17, // 62: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 23, // 63: resource.ResourceStore.List:input_type -> resource.ListRequest + 25, // 64: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 27, // 65: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest + 31, // 66: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest + 29, // 67: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest + 35, // 68: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest + 33, // 69: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest + 42, // 70: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest + 44, // 71: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest + 37, // 72: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 20, // 73: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 14, // 74: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 16, // 75: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 18, // 76: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 24, // 77: resource.ResourceStore.List:output_type -> resource.ListResponse + 26, // 78: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 28, // 79: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse + 32, // 80: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse + 30, // 81: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse + 36, // 82: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse + 34, // 83: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse + 43, // 84: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse + 45, // 85: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse + 38, // 86: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 73, // [73:87] is the sub-list for method output_type + 59, // [59:73] is the sub-list for method input_type + 59, // [59:59] is the sub-list for extension type_name + 59, // [59:59] is the sub-list for extension extendee + 0, // [0:59] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -4811,13 +4745,14 @@ func file_resource_proto_init() { if File_resource_proto != nil { return } + file_resource_proto_msgTypes[15].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)), - NumEnums: 7, - NumMessages: 53, + NumEnums: 8, + NumMessages: 51, NumExtensions: 0, NumServices: 6, }, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index 830c2a4d890..f5111d90657 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -177,8 +177,6 @@ message ReadRequest { // Optionally pick an explicit resource version int64 resource_version = 2; - // Optionally decide to return the latest RV if deleted - bool include_deleted = 3; } message ReadResponse { @@ -220,10 +218,19 @@ message ListOptions { } enum ResourceVersionMatch { - NotOlderThan = 0; - Exact = 1; + // Deprecated, use ResourceVersionMatch V2 + DEPRECATED_NotOlderThan = 0; + DEPRECATED_Exact = 1; } +enum ResourceVersionMatchV2 { + UNKNOWN = 0; + Unset = 1; + Exact = 2; + NotOlderThan = 3; +} + + message ListRequest { enum Source { STORE = 0; // the standard place @@ -238,7 +245,8 @@ message ListRequest { int64 resource_version = 2; // List options - ResourceVersionMatch version_match = 3; + // DEPRECATED - use version_match_v2 + optional ResourceVersionMatch version_match = 3; // Maximum number of items to return // NOTE responses will also be limited by the response payload size @@ -249,6 +257,8 @@ message ListRequest { // Select values from history or trash Source source = 6; + + ResourceVersionMatchV2 version_match_v2 = 7; } message ListResponse { @@ -702,26 +712,6 @@ message ResourceTableRow { bytes object = 4; } -//---------------------------- -// Restore Support -//---------------------------- - -message RestoreRequest { - // Full key must be set - ResourceKey key = 1; - - // The resource version to restore - int64 resource_version = 2; -} - -message RestoreResponse { - // Error details - ErrorResult error = 1; - - // The updated resource version - int64 resource_version = 2; -} - //---------------------------- // Blob Support //---------------------------- @@ -811,7 +801,6 @@ service ResourceStore { rpc Create(CreateRequest) returns (CreateResponse); rpc Update(UpdateRequest) returns (UpdateResponse); rpc Delete(DeleteRequest) returns (DeleteResponse); - rpc Restore(RestoreRequest) returns (RestoreResponse); // The results *may* include values that should not be returned to the user // This will perform best-effort filtering to increase performace. diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index 25c4a937f9c..4186147ef7c 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -19,13 +19,12 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - ResourceStore_Read_FullMethodName = "/resource.ResourceStore/Read" - ResourceStore_Create_FullMethodName = "/resource.ResourceStore/Create" - ResourceStore_Update_FullMethodName = "/resource.ResourceStore/Update" - ResourceStore_Delete_FullMethodName = "/resource.ResourceStore/Delete" - ResourceStore_Restore_FullMethodName = "/resource.ResourceStore/Restore" - ResourceStore_List_FullMethodName = "/resource.ResourceStore/List" - ResourceStore_Watch_FullMethodName = "/resource.ResourceStore/Watch" + ResourceStore_Read_FullMethodName = "/resource.ResourceStore/Read" + ResourceStore_Create_FullMethodName = "/resource.ResourceStore/Create" + ResourceStore_Update_FullMethodName = "/resource.ResourceStore/Update" + ResourceStore_Delete_FullMethodName = "/resource.ResourceStore/Delete" + ResourceStore_List_FullMethodName = "/resource.ResourceStore/List" + ResourceStore_Watch_FullMethodName = "/resource.ResourceStore/Watch" ) // ResourceStoreClient is the client API for ResourceStore service. @@ -41,7 +40,6 @@ type ResourceStoreClient interface { Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*UpdateResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) - Restore(ctx context.Context, in *RestoreRequest, opts ...grpc.CallOption) (*RestoreResponse, error) // The results *may* include values that should not be returned to the user // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering @@ -100,16 +98,6 @@ func (c *resourceStoreClient) Delete(ctx context.Context, in *DeleteRequest, opt return out, nil } -func (c *resourceStoreClient) Restore(ctx context.Context, in *RestoreRequest, opts ...grpc.CallOption) (*RestoreResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(RestoreResponse) - err := c.cc.Invoke(ctx, ResourceStore_Restore_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *resourceStoreClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListResponse) @@ -166,7 +154,6 @@ type ResourceStoreServer interface { Create(context.Context, *CreateRequest) (*CreateResponse, error) Update(context.Context, *UpdateRequest) (*UpdateResponse, error) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) - Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) // The results *may* include values that should not be returned to the user // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering @@ -193,9 +180,6 @@ func (UnimplementedResourceStoreServer) Update(context.Context, *UpdateRequest) func (UnimplementedResourceStoreServer) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") } -func (UnimplementedResourceStoreServer) Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented") -} func (UnimplementedResourceStoreServer) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method List not implemented") } @@ -286,24 +270,6 @@ func _ResourceStore_Delete_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } -func _ResourceStore_Restore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RestoreRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ResourceStoreServer).Restore(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ResourceStore_Restore_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ResourceStoreServer).Restore(ctx, req.(*RestoreRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _ResourceStore_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListRequest) if err := dec(in); err != nil { @@ -366,10 +332,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ MethodName: "Delete", Handler: _ResourceStore_Delete_Handler, }, - { - MethodName: "Restore", - Handler: _ResourceStore_Restore_Handler, - }, { MethodName: "List", Handler: _ResourceStore_List_Handler, diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 0a983de75aa..0509c938382 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -10,14 +10,12 @@ import ( "sync/atomic" "time" - "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -393,6 +391,7 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour if oldValue == nil { event.Type = WatchEvent_ADDED } else { + event.Type = WatchEvent_MODIFIED check.Verb = utils.VerbUpdate temp := &unstructured.Unstructured{} @@ -404,13 +403,6 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour if err != nil { return nil, AsErrorResult(err) } - - // restores will restore with a different k8s uid - if event.ObjectOld.GetUID() != obj.GetUID() { - event.Type = WatchEvent_ADDED - } else { - event.Type = WatchEvent_MODIFIED - } } if key.Namespace != obj.GetNamespace() { @@ -761,10 +753,11 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err rsp.Items = append(rsp.Items, item) if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { t := iter.ContinueToken() - if req.Source == ListRequest_HISTORY { - // history lists in desc order, so the continue token takes the - // final RV in the list, and then will start from there in the next page, - // rather than the lists first RV + if req.Source == ListRequest_HISTORY || req.Source == ListRequest_TRASH { + // For history lists, we need to use the current RV in the continue token + // to ensure consistent pagination. The order depends on VersionMatch: + // - NotOlderThan: ascending order (oldest to newest) + // - Unset: descending order (newest to oldest) t = iter.ContinueTokenWithCurrentRV() } if iter.Next() { @@ -792,126 +785,6 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err return rsp, err } -func (s *server) Restore(ctx context.Context, req *RestoreRequest) (*RestoreResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.List") - defer span.End() - - // check that the user has access - user, ok := claims.AuthInfoFrom(ctx) - if !ok || user == nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Message: "no user found in context", - Code: http.StatusUnauthorized, - }}, nil - } - - if err := s.Init(ctx); err != nil { - return nil, err - } - - checker, err := s.access.Compile(ctx, user, claims.ListRequest{ - Group: req.Key.Group, - Resource: req.Key.Resource, - Namespace: req.Key.Namespace, - Verb: utils.VerbGet, - }) - if err != nil { - return &RestoreResponse{Error: AsErrorResult(err)}, nil - } - if checker == nil { - return &RestoreResponse{Error: &ErrorResult{ - Code: http.StatusForbidden, - }}, nil - } - - // get the asked for resource version to restore - readRsp, err := s.Read(ctx, &ReadRequest{ - Key: req.Key, - ResourceVersion: req.ResourceVersion, - IncludeDeleted: true, - }) - if err != nil || readRsp == nil || readRsp.Error != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusNotFound, - Message: fmt.Sprintf("could not find old resource: %s", readRsp.Error.Message), - }, - }, nil - } - - // generate a new k8s UID when restoring. The name will remain the same - // (for dashboards, this will be the dashboard uid), but since controllers - // will see this as a create event, we do not want the same k8s UID, or - // there may be unintended behavior - newUid := types.UID(uuid.NewString()) - tmp := &unstructured.Unstructured{} - err = tmp.UnmarshalJSON(readRsp.Value) - if err != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusNotFound, - Message: fmt.Sprintf("could not unmarhsal: %s", err.Error()), - }, - }, nil - } - obj, err := utils.MetaAccessor(tmp) - if err != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusNotFound, - Message: fmt.Sprintf("could not get object: %s", err.Error()), - }, - }, nil - } - obj.SetUID(newUid) - - rtObj, ok := obj.GetRuntimeObject() - if !ok { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusNotFound, - Message: "could not get runtime object", - }, - }, nil - } - - newObj, err := json.Marshal(rtObj) - if err != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusNotFound, - Message: fmt.Sprintf("could not marshal object: %s", err.Error()), - }, - }, nil - } - - // finally, send to the backend to create & update the history of the restored object - event, errRes := s.newEvent(ctx, user, req.Key, newObj, readRsp.Value) - if errRes != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusInternalServerError, - Message: fmt.Sprintf("could not create restore resource event: %s", errRes.Message), - }, - }, nil - } - rv, err := s.backend.WriteEvent(ctx, *event) - if err != nil { - return &RestoreResponse{ - Error: &ErrorResult{ - Code: http.StatusInternalServerError, - Message: fmt.Sprintf("could not restore resource: %s", err.Error()), - }, - }, nil - } - - return &RestoreResponse{ - Error: nil, - ResourceVersion: rv, - }, nil -} - func (s *server) initWatcher() error { var err error s.broadcaster, err = NewBroadcaster(s.ctx, func(out chan<- *WrittenEvent) error { diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 3e204717040..bb42d73b12b 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -233,79 +233,4 @@ func TestSimpleServer(t *testing.T) { ResourceVersion: created.ResourceVersion}) require.ErrorIs(t, err, ErrOptimisticLockingFailed) }) - - t.Run("playlist restore", func(t *testing.T) { - uid := "zzz" - raw := []byte(`{ - "apiVersion": "playlist.grafana.app/v0alpha1", - "kind": "Playlist", - "metadata": { - "name": "fdgsv37qslr0ga", - "namespace": "default", - "uid": "` + uid + `", - "annotations": { - "grafana.app/repoName": "elsewhere", - "grafana.app/repoPath": "path/to/item", - "grafana.app/repoTimestamp": "2024-02-02T00:00:00Z" - } - }, - "spec": { - "title": "hello", - "interval": "5m", - "items": [ - { - "type": "dashboard_by_uid", - "value": "vmie2cmWz" - } - ] - } - }`) - - key := &ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", - Namespace: "default", - Name: "fdgsv37qslr0ga", - } - - // create - created, err := server.Create(ctx, &CreateRequest{ - Value: raw, - Key: key, - }) - require.NoError(t, err) - - // make sure it exists - found, err := server.Read(ctx, &ReadRequest{Key: key}) - require.NoError(t, err) - require.Nil(t, found.Error) - fmt.Println(found.ResourceVersion) - - // delete it - deleted, err := server.Delete(ctx, &DeleteRequest{Key: key, ResourceVersion: created.ResourceVersion}) - require.NoError(t, err) - require.True(t, deleted.ResourceVersion > created.ResourceVersion) - - // restore it - restored, err := server.Restore(ctx, &RestoreRequest{ - Key: key, - ResourceVersion: found.ResourceVersion, - }) - require.NoError(t, err) - require.Nil(t, restored.Error) - require.True(t, restored.ResourceVersion > deleted.ResourceVersion) - - // ensure it exists now - found, err = server.Read(ctx, &ReadRequest{Key: key}) - require.NoError(t, err) - require.Nil(t, found.Error) - require.Equal(t, restored.ResourceVersion, found.ResourceVersion) - foundUnstructured := &unstructured.Unstructured{} - err = foundUnstructured.UnmarshalJSON(found.Value) - require.NoError(t, err) - foundObj, err := utils.MetaAccessor(foundUnstructured) - require.NoError(t, err) - // the UID should be different now - require.NotEqual(t, uid, string(foundObj.GetUID())) - }) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 4976449f548..e99df0870e3 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -159,7 +159,7 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, var index bleve.Index build := true - mapper, err := getBleveMappings(fields) + mapper, err := GetBleveMappings(fields) if err != nil { return nil, err } @@ -660,8 +660,14 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res } } - // Add a text query - if req.Query != "" && req.Query != "*" { + if len(req.Query) > 1 && strings.Contains(req.Query, "*") { + // wildcard query is expensive - should be used with caution + wildcard := bleve.NewWildcardQuery(req.Query) + queries = append(queries, wildcard) + } + + if req.Query != "" && !strings.Contains(req.Query, "*") { + // Add a text query searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) // There are multiple ways to match the query string to documents. The following queries are ordered by priority: @@ -789,6 +795,11 @@ var textSortFields = map[string]string{ const lowerCase = "phrase" +// termField fields to use termQuery for filtering +var termFields = []string{ + resource.SEARCH_FIELD_TITLE, +} + // Convert a "requirement" into a bleve query func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *resource.ErrorResult) { switch selection.Operator(req.Operator) { @@ -797,16 +808,14 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r return query.NewMatchAllQuery(), nil } - if len(req.Values[0]) == 1 { - q := query.NewMatchQuery(filterValue(req.Key, req.Values[0])) - q.FieldVal = prefix + req.Key - return q, nil + if len(req.Values) == 1 { + filter := filterValue(req.Key, req.Values[0]) + return newQuery(req.Key, filter, prefix), nil } conjuncts := []query.Query{} for _, v := range req.Values { - q := query.NewMatchQuery(filterValue(req.Key, v)) - q.FieldVal = prefix + req.Key + q := newQuery(req.Key, filterValue(req.Key, v), prefix) conjuncts = append(conjuncts, q) } @@ -822,15 +831,13 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r return query.NewMatchAllQuery(), nil } if len(req.Values) == 1 { - q := query.NewMatchQuery(filterValue(req.Key, req.Values[0])) - q.FieldVal = prefix + req.Key + q := newQuery(req.Key, filterValue(req.Key, req.Values[0]), prefix) return q, nil } disjuncts := []query.Query{} for _, v := range req.Values { - q := query.NewMatchQuery(filterValue(req.Key, v)) - q.FieldVal = prefix + req.Key + q := newQuery(req.Key, filterValue(req.Key, v), prefix) disjuncts = append(disjuncts, q) } @@ -841,7 +848,8 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r var mustNotQueries []query.Query for _, value := range req.Values { - mustNotQueries = append(mustNotQueries, bleve.NewMatchQuery(filterValue(req.Key, value))) + q := newQuery(req.Key, filterValue(req.Key, value), prefix) + mustNotQueries = append(mustNotQueries, q) } boolQuery.AddMustNot(mustNotQueries...) @@ -856,6 +864,55 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r ) } +// newQuery will create a query that will match the value or the tokens of the value +func newQuery(key string, value string, prefix string) query.Query { + if value == "*" { + return bleve.NewMatchAllQuery() + } + if strings.Contains(value, "*") { + // wildcard query is expensive - should be used with caution + return bleve.NewWildcardQuery(value) + } + delimiter, ok := hasTerms(value) + if slices.Contains(termFields, key) && ok { + return newTermsQuery(key, value, delimiter, prefix) + } + q := bleve.NewMatchQuery(value) + q.SetField(prefix + key) + return q +} + +// newTermsQuery will create a query that will match on term or tokens +func newTermsQuery(key string, value string, delimiter string, prefix string) query.Query { + tokens := strings.Split(value, delimiter) + // won't match with ending space + value = strings.TrimSuffix(value, " ") + + q := bleve.NewTermQuery(value) + q.SetField(prefix + key) + + cq := newMatchAllTokensQuery(tokens, key, prefix) + return bleve.NewDisjunctionQuery(q, cq) +} + +// newMatchAllTokensQuery will create a query that will match on all tokens +func newMatchAllTokensQuery(tokens []string, key string, prefix string) query.Query { + cq := bleve.NewConjunctionQuery() + for _, token := range tokens { + _, ok := hasTerms(token) + if ok { + tq := bleve.NewTermQuery(token) + tq.SetField(prefix + key) + cq.AddQuery(tq) + continue + } + mq := bleve.NewMatchQuery(token) + mq.SetField(prefix + key) + cq.AddQuery(mq) + } + return cq +} + // filterValue will convert the value to lower case if the field is a phrase field func filterValue(field string, v string) string { if strings.HasSuffix(field, lowerCase) { @@ -1068,3 +1125,20 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade return filteringSearcher, nil } + +// hasTerms - any value that will be split into multiple tokens +var hasTerms = func(v string) (string, bool) { + for _, c := range TermCharacters { + if strings.Contains(v, c) { + return c, true + } + } + return "", false +} + +// TermCharacters characters that will be used to determine if a value is split into tokens +var TermCharacters = []string{ + " ", "-", "_", ".", ",", ":", ";", "?", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", + "=", "{", "}", "[", "]", "|", "\\", "/", "<", ">", "~", "`", + "'", "\"", +} diff --git a/pkg/storage/unified/search/bleve_mappings.go b/pkg/storage/unified/search/bleve_mappings.go index c9afce91a09..3a8c4e75420 100644 --- a/pkg/storage/unified/search/bleve_mappings.go +++ b/pkg/storage/unified/search/bleve_mappings.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" ) -func getBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) { +func GetBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) { mapper := bleve.NewIndexMapping() err := RegisterCustomAnalyzers(mapper) @@ -31,22 +31,22 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM } mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_NAME, nameMapping) + // for sorting by title full phrase + titlePhraseMapping := bleve.NewKeywordFieldMapping() + titlePhraseMapping.Store = false // already stored in title + mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping) + // for searching by title - uses an edge ngram token filter titleSearchMapping := bleve.NewTextFieldMapping() titleSearchMapping.Analyzer = TITLE_ANALYZER titleSearchMapping.Store = false // already stored in title - mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_NGRAM, titleSearchMapping) // mapping for title to search on words/tokens larger than the ngram size titleWordMapping := bleve.NewTextFieldMapping() titleWordMapping.Analyzer = standard.Name titleWordMapping.Store = true - mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleWordMapping) - - // for filtering/sorting by title full phrase - titlePhraseMapping := bleve.NewKeywordFieldMapping() - titleSearchMapping.Store = false // already stored in title - mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping) + // NOTE: this causes 3 title fields in the response + mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleWordMapping, titleSearchMapping, titlePhraseMapping) descriptionMapping := &mapping.FieldMapping{ Name: resource.SEARCH_FIELD_DESCRIPTION, diff --git a/pkg/storage/unified/search/bleve_mappings_test.go b/pkg/storage/unified/search/bleve_mappings_test.go index 9c1c6c7f8ad..3b8027ee06e 100644 --- a/pkg/storage/unified/search/bleve_mappings_test.go +++ b/pkg/storage/unified/search/bleve_mappings_test.go @@ -1,4 +1,4 @@ -package search +package search_test import ( "fmt" @@ -9,10 +9,11 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" ) func TestDocumentMapping(t *testing.T) { - mappings, err := getBleveMappings(nil) + mappings, err := search.GetBleveMappings(nil) require.NoError(t, err) data := resource.IndexableDocument{ Title: "title", @@ -48,5 +49,5 @@ func TestDocumentMapping(t *testing.T) { fmt.Printf("DOC: fields %d\n", len(doc.Fields)) fmt.Printf("DOC: size %d\n", doc.Size()) - require.Equal(t, 16, len(doc.Fields)) + require.Equal(t, 17, len(doc.Fields)) } diff --git a/pkg/storage/unified/search/bleve_performance_test.go b/pkg/storage/unified/search/bleve_performance_test.go new file mode 100644 index 00000000000..2d645235259 --- /dev/null +++ b/pkg/storage/unified/search/bleve_performance_test.go @@ -0,0 +1,131 @@ +package search_test + +import ( + "context" + "fmt" + "os" + "runtime" + "testing" + "time" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" +) + +func setupIndex() (resource.ResourceIndex, string) { + // size := 1000000 // TODO: 200k documents standard size? + size := 200000 + // batchSize := 1000 slower 8s (for 200k documents) - 34s (for 1M documents) + // batchSize := 10000 // faster 5s (for 200k documents) - 27s (for 1M documents) + batchSize := 100000 // fasterer 3.5s (for 200k documents) - 27s (for 1M documents) + writer := newTestWriter(size, batchSize) + return newTestDashboardsIndex(nil, 1, int64(size), int64(batchSize), writer) +} + +const maxAllowedTime = 20 * time.Millisecond // Reasonable (can vary per env) performance threshold per query (e.g., 20ms) +const maxAllowedAllocMB = 1 // 1MB memory per operation +const maxAllowedAlloc = maxAllowedAllocMB * 1024 * 1024 +const verbose = false + +// BenchmarkBleveQuery measures the time, mem, cpu to execute a search query +// changes the the indexer settings can cause unforeseen performance issues ( for example: using wildcard queries ) +// this will fail if the stats exceed the "normal" thresholds +func BenchmarkBleveQuery(b *testing.B) { + var memStatsStart runtime.MemStats + var memStatsAfterIndex runtime.MemStats + runtime.ReadMemStats(&memStatsStart) + + testIndex, testIndexDir := setupIndex() + defer func() { + err := os.RemoveAll(testIndexDir) + if err != nil { + fmt.Printf("Error removing index directory: %v\n", err) + } + }() + + runtime.ReadMemStats(&memStatsAfterIndex) + + allocDiff := memStatsAfterIndex.Alloc - memStatsStart.Alloc + + logVerbose(fmt.Sprintf("Memory allocated for index: %d bytes", allocDiff)) + + searchRequest := newQueryByTitle("name99999") + + b.ResetTimer() // Reset timer before benchmarking + b.ReportAllocs() // Track memory allocations + + for i := 0; i < b.N; i++ { + start := time.Now() // Start timer + var memStatsBefore, memStatsAfter runtime.MemStats + runtime.ReadMemStats(&memStatsBefore) + + _, err := testIndex.Search(context.Background(), nil, searchRequest, nil) + + elapsed := time.Since(start) // Calculate elapsed time + runtime.ReadMemStats(&memStatsAfter) + allocDiff := (memStatsAfter.Alloc - memStatsBefore.Alloc) + if memStatsAfter.Alloc < memStatsBefore.Alloc { + // This can happen due to memory being freed after the search operation + allocDiff = 0 // don't care if it goes down + } + + logVerbose(fmt.Sprintf("Memory allocated for query: %d bytes", allocDiff)) + + require.NoError(b, err) + + // Fail if query takes longer than maxAllowedTime + if elapsed > maxAllowedTime { + b.Fatalf("Query too slow: %v (limit: %v)", elapsed, maxAllowedTime) + } + // Check memory allocation limit + if allocDiff > maxAllowedAlloc { + b.Fatalf("Excessive memory usage: %d mb (limit: %d mb)", allocDiff, maxAllowedAllocMB) + } + } +} + +func newTestWriter(size int, batchSize int) IndexWriter { + key := &resource.ResourceKey{ + Namespace: "default", + Group: "dashboard.grafana.app", + Resource: "dashboards", + } + + return func(index resource.ResourceIndex) (int64, error) { + total := time.Now() + start := time.Now() + for i := range size { + name := fmt.Sprintf("name%d", i) + err := index.Write(&resource.IndexableDocument{ + RV: int64(i), + Name: name, + Key: &resource.ResourceKey{ + Name: name, + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }, + Title: name + "-title", + }) + if err != nil { + return 0, err + } + // show progress for every batch + if i%batchSize == 0 && verbose { + fmt.Printf("Indexed %d documents\n", i) + end := time.Now() + fmt.Printf("Time taken for indexing batch: %s\n", end.Sub(start)) + start = time.Now() + } + } + end := time.Now() + logVerbose(fmt.Sprintf("Indexed %d documents in %s", size, end.Sub(total))) + return 0, nil + } +} + +func logVerbose(msg string) { + if verbose { + fmt.Println(msg) + } +} diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index 29287de97e4..ae1782c61a2 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -1,4 +1,4 @@ -package search +package search_test import ( "context" @@ -16,8 +16,11 @@ import ( "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" ) +const threshold = 9999 + func TestCanSearchByTitle(t *testing.T) { key := &resource.ResourceKey{ Namespace: "default", @@ -26,7 +29,7 @@ func TestCanSearchByTitle(t *testing.T) { } t.Run("when query is empty, sort documents by title instead of search score", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -53,7 +56,7 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // search for phrase - query := newQuery("") + query := newTestQuery("") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(2), res.TotalHits) @@ -61,7 +64,7 @@ func TestCanSearchByTitle(t *testing.T) { }) t.Run("will boost phrase match query over match query results", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -88,7 +91,7 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // search for phrase - query := newQuery("want hello") + query := newTestQuery("want hello") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(2), res.TotalHits) @@ -96,7 +99,7 @@ func TestCanSearchByTitle(t *testing.T) { }) t.Run("will prioritize matches", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -122,7 +125,7 @@ func TestCanSearchByTitle(t *testing.T) { }) require.NoError(t, err) - query := newQuery("New dash") + query := newTestQuery("New dash") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(2), res.TotalHits) @@ -130,7 +133,7 @@ func TestCanSearchByTitle(t *testing.T) { }) t.Run("will boost exact match query over match phrase query results", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -157,7 +160,7 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // search for exact match - query := newQuery("we want hello") + query := newTestQuery("we want hello") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(2), res.TotalHits) @@ -165,7 +168,7 @@ func TestCanSearchByTitle(t *testing.T) { }) t.Run("title with numbers will match document", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -180,20 +183,65 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // search for prefix of title with mix of chars and numbers - query := newQuery("A12") + query := newQueryByTitle("A12") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // search for whole title - query = newQuery("A123456") + query = newQueryByTitle("A123456") + res, err = index.Search(context.Background(), nil, query, nil) + require.NoError(t, err) + require.Equal(t, int64(1), res.TotalHits) + + // case insensive search for partial title + query = newQueryByTitle("a1234") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) }) + t.Run("title will match escaped characters", func(t *testing.T) { + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) + err := index.Write(&resource.IndexableDocument{ + RV: 1, + Name: "name1", + Key: &resource.ResourceKey{ + Name: "aaa", + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }, + Title: "what\"s up", + }) + require.NoError(t, err) + + err = index.Write(&resource.IndexableDocument{ + RV: 2, + Name: "name2", + Key: &resource.ResourceKey{ + Name: "name2", + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }, + Title: "what\"s that", + }) + require.NoError(t, err) + + query := newQueryByTitle("what\"s up") + res, err := index.Search(context.Background(), nil, query, nil) + require.NoError(t, err) + require.Equal(t, int64(1), res.TotalHits) + + query = newQueryByTitle("what\"s") + res, err = index.Search(context.Background(), nil, query, nil) + require.NoError(t, err) + require.Equal(t, int64(2), res.TotalHits) + }) + t.Run("title search will match document", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -208,50 +256,50 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // search by entire phrase - query := newQuery("I want to say a wonderfully Hello to the WORLD! Hello-world") + query := newTestQuery("I want to say a wonderfully Hello to the WORLD! Hello-world") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // search for word at start - query = newQuery("hello") + query = newTestQuery("hello") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // search for word larger than ngram max size - query = newQuery("wonderfully") + query = newQueryByTitle("wonderfully") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // search for word at end - query = newQuery("world") + query = newQueryByTitle("world") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // can search for word substring anchored at start of word (edge ngram) - query = newQuery("worl") + query = newQueryByTitle("worl") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // can search for multiple, non-consecutive words in title - query = newQuery("hello world") + query = newQueryByTitle("hello world") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) // can search for a term with a hyphen - query = newQuery("hello-world") + query = newQueryByTitle("hello-world") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(1), res.TotalHits) }) t.Run("title search will NOT match documents", func(t *testing.T) { - index := newTestDashboardsIndex(t) + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) err := index.Write(&resource.IndexableDocument{ RV: 1, Name: "name1", @@ -290,26 +338,79 @@ func TestCanSearchByTitle(t *testing.T) { require.NoError(t, err) // word that doesn't exist - query := newQuery("cats") + query := newQueryByTitle("cats") res, err := index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(0), res.TotalHits) // string shorter than 3 chars (ngam min) - query = newQuery("ma") + query = newQueryByTitle("ma") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(0), res.TotalHits) // substring that doesn't exist - query = newQuery("A01") + query = newQueryByTitle("A01") res, err = index.Search(context.Background(), nil, query, nil) require.NoError(t, err) require.Equal(t, int64(0), res.TotalHits) }) + + t.Run("title search with character will match one document", func(t *testing.T) { + index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop) + err := index.Write(&resource.IndexableDocument{ + RV: 1, + Name: "name1", + Key: &resource.ResourceKey{ + Name: "aaa", + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }, + Title: "foo", + }) + require.NoError(t, err) + + for i, v := range search.TermCharacters { + err = index.Write(&resource.IndexableDocument{ + RV: int64(i), + Name: fmt.Sprintf("name%d", i), + Key: &resource.ResourceKey{ + Name: fmt.Sprintf("name%d", i), + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }, + Title: fmt.Sprintf(`test foo%d%sbar`, i, v), + }) + require.NoError(t, err) + } + + for i, v := range search.TermCharacters { + title := fmt.Sprintf(`test foo%d%sbar`, i, v) + query := newQueryByTitle(title) + res, err := index.Search(context.Background(), nil, query, nil) + require.NoError(t, err) + if res.TotalHits != 1 { + fmt.Printf("i: %d, v: %s, title: %s", i, v, title) + } + require.Equal(t, int64(1), res.TotalHits) + + // can search for a title with a term character suffix + title = fmt.Sprintf(`foo%d%s`, i, v) + query = newQueryByTitle(title) + res, err = index.Search(context.Background(), nil, query, nil) + require.NoError(t, err) + if res.TotalHits != 1 { + fmt.Printf("i: %d, v: %s, title: %s", i, v, title) + } + + require.Equal(t, int64(1), res.TotalHits) + } + }) } -func newQuery(query string) *resource.ResourceSearchRequest { +func newTestQuery(query string) *resource.ResourceSearchRequest { return &resource.ResourceSearchRequest{ Options: &resource.ListOptions{ Key: &resource.ResourceKey{ @@ -323,7 +424,21 @@ func newQuery(query string) *resource.ResourceSearchRequest { } } -func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex { +func newQueryByTitle(query string) *resource.ResourceSearchRequest { + return &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + Fields: []*resource.Requirement{{Key: "title", Operator: "=", Values: []string{query}}}, + }, + Limit: 100000, + } +} + +func newTestDashboardsIndex(t TB, threshold int64, size int64, batchSize int64, writer IndexWriter) (resource.ResourceIndex, string) { key := &resource.ResourceKey{ Namespace: "default", Group: "dashboard.grafana.app", @@ -332,17 +447,18 @@ func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex { tmpdir, err := os.MkdirTemp("", "grafana-bleve-test") require.NoError(t, err) - backend, err := NewBleveBackend(BleveOptions{ + backend, err := search.NewBleveBackend(search.BleveOptions{ Root: tmpdir, - FileThreshold: 9999, // use in-memory for tests + FileThreshold: threshold, // use in-memory for tests + BatchSize: int(batchSize), }, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering), nil) require.NoError(t, err) rv := int64(10) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"}) - info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - return &DashboardDocumentBuilder{ + info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + return &search.DashboardDocumentBuilder{ Namespace: namespace, Blob: blob, Stats: make(map[string]map[string]int64), // empty stats @@ -355,10 +471,16 @@ func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex { Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, - }, 2, rv, info.Fields, func(index resource.ResourceIndex) (int64, error) { return 0, nil }) + }, size, rv, info.Fields, writer) require.NoError(t, err) - return index + return index, tmpdir +} + +type IndexWriter func(index resource.ResourceIndex) (int64, error) + +var noop IndexWriter = func(index resource.ResourceIndex) (int64, error) { + return 0, nil } // helper to check which tokens are generated by an analyzer @@ -399,3 +521,15 @@ func debugIndexedTerms(index bleve.Index, field string) { } } } + +// TB is an interface that works for both *testing.T and *testing.B +type TB interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Helper() + FailNow() +} diff --git a/pkg/storage/unified/search/document_test.go b/pkg/storage/unified/search/document_test.go index c11ada997a6..49c7e0c9e89 100644 --- a/pkg/storage/unified/search/document_test.go +++ b/pkg/storage/unified/search/document_test.go @@ -1,4 +1,4 @@ -package search +package search_test import ( "context" @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" ) func doSnapshotTests(t *testing.T, builder resource.DocumentBuilder, kind string, key *resource.ResourceKey, names []string) { @@ -52,14 +53,14 @@ func TestDashboardDocumentBuilder(t *testing.T) { Resource: "dashboards", } - info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - return &DashboardDocumentBuilder{ + info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + return &search.DashboardDocumentBuilder{ Namespace: namespace, Blob: blob, Stats: map[string]map[string]int64{ "aaa": { - DASHBOARD_ERRORS_LAST_1_DAYS: 1, - DASHBOARD_ERRORS_LAST_7_DAYS: 1, + search.DASHBOARD_ERRORS_LAST_1_DAYS: 1, + search.DASHBOARD_ERRORS_LAST_7_DAYS: 1, }, }, DatasourceLookup: dashboard.CreateDatasourceLookup([]*dashboard.DatasourceQueryResult{{ diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 53cf85ef9f5..d675998f97a 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -88,6 +88,7 @@ type pruningKey struct { namespace string group string resource string + name string } // Small abstraction to allow for different pruner implementations. @@ -215,6 +216,7 @@ func (b *backend) initPruner(ctx context.Context) error { Namespace: key.namespace, Group: key.group, Resource: key.resource, + Name: key.name, }, }) if err != nil { @@ -228,6 +230,7 @@ func (b *backend) initPruner(ctx context.Context) error { "namespace", key.namespace, "group", key.group, "resource", key.resource, + "name", key.name, "rows", rows) return nil }) @@ -237,6 +240,7 @@ func (b *backend) initPruner(ctx context.Context) error { "namespace", key.namespace, "group", key.group, "resource", key.resource, + "name", key.name, "error", err) }, Reg: b.reg, @@ -304,9 +308,6 @@ func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (in // TODO: validate key ? switch event.Type { case resource.WatchEvent_ADDED: - if event.ObjectOld != nil { - return b.restore(ctx, event) - } return b.create(ctx, event) case resource.WatchEvent_MODIFIED: return b.update(ctx, event) @@ -347,7 +348,12 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, }); err != nil { return guid, fmt.Errorf("insert into resource history: %w", err) } - _ = b.historyPruner.Add(pruningKey{namespace: event.Key.Namespace, group: event.Key.Group, resource: event.Key.Resource}) + _ = b.historyPruner.Add(pruningKey{ + namespace: event.Key.Namespace, + group: event.Key.Group, + resource: event.Key.Resource, + name: event.Key.Name, + }) if b.simulatedNetworkLatency > 0 { time.Sleep(b.simulatedNetworkLatency) } @@ -401,7 +407,12 @@ func (b *backend) update(ctx context.Context, event resource.WriteEvent) (int64, }); err != nil { return guid, fmt.Errorf("insert into resource history: %w", err) } - _ = b.historyPruner.Add(pruningKey{namespace: event.Key.Namespace, group: event.Key.Group, resource: event.Key.Resource}) + _ = b.historyPruner.Add(pruningKey{ + namespace: event.Key.Namespace, + group: event.Key.Group, + resource: event.Key.Resource, + name: event.Key.Name, + }) return guid, nil }) @@ -449,69 +460,12 @@ func (b *backend) delete(ctx context.Context, event resource.WriteEvent) (int64, }); err != nil { return guid, fmt.Errorf("insert into resource history: %w", err) } - _ = b.historyPruner.Add(pruningKey{namespace: event.Key.Namespace, group: event.Key.Group, resource: event.Key.Resource}) - return guid, nil - }) - - if err != nil { - return 0, err - } - - b.notifier.send(ctx, &resource.WrittenEvent{ - Type: event.Type, - Key: event.Key, - PreviousRV: event.PreviousRV, - Value: event.Value, - ResourceVersion: rv, - Folder: folder, - }) - - return rv, nil -} - -func (b *backend) restore(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"Restore") - defer span.End() - guid := uuid.New().String() - folder := "" - if event.Object != nil { - folder = event.Object.GetFolder() - } - rv, err := b.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) { - // 1. Re-create resource - // Note: we may want to replace the write event with a create event, tbd. - if _, err := dbutil.Exec(ctx, tx, sqlResourceInsert, sqlResourceRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: event, - Folder: folder, - GUID: guid, - }); err != nil { - return guid, fmt.Errorf("insert into resource: %w", err) - } - - // 2. Insert into resource history - if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: event, - Folder: folder, - GUID: guid, - }); err != nil { - return guid, fmt.Errorf("insert into resource history: %w", err) - } - _ = b.historyPruner.Add(pruningKey{namespace: event.Key.Namespace, group: event.Key.Group, resource: event.Key.Resource}) - - // 3. Update all resource history entries with the new UID - // Note: we do not update any history entries that have a deletion timestamp included. This will become - // important once we start using finalizers, as the initial delete will show up as an update with a deletion timestamp included. - if _, err := dbutil.Exec(ctx, tx, sqlResoureceHistoryUpdateUid, sqlResourceHistoryUpdateRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: event, - OldUID: string(event.ObjectOld.GetUID()), - NewUID: string(event.Object.GetUID()), - }); err != nil { - return guid, fmt.Errorf("update history uid: %w", err) - } - + _ = b.historyPruner.Add(pruningKey{ + namespace: event.Key.Namespace, + group: event.Key.Group, + resource: event.Key.Resource, + name: event.Key.Name, + }) return guid, nil }) @@ -553,17 +507,6 @@ func (b *backend) ReadResource(ctx context.Context, req *resource.ReadRequest) * err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { var err error res, err = dbutil.QueryRow(ctx, tx, sr, readReq) - // if not found, look for latest deleted version (if requested) - if errors.Is(err, sql.ErrNoRows) && req.IncludeDeleted { - sr = sqlResourceHistoryRead - readReq2 := &sqlResourceReadRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - Request: req, - Response: NewReadResponse(), - } - res, err = dbutil.QueryRow(ctx, tx, sr, readReq2) - return err - } return err }) @@ -601,9 +544,10 @@ func (b *backend) ListIterator(ctx context.Context, req *resource.ListRequest, c } type listIter struct { - rows db.Rows - offset int64 - listRV int64 + rows db.Rows + offset int64 + listRV int64 + sortAsc bool // any error err error @@ -618,11 +562,11 @@ type listIter struct { // ContinueToken implements resource.ListIterator. func (l *listIter) ContinueToken() string { - return resource.ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset}.String() + return resource.ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset, SortAscending: l.sortAsc}.String() } func (l *listIter) ContinueTokenWithCurrentRV() string { - return resource.ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset}.String() + return resource.ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset, SortAscending: l.sortAsc}.String() } func (l *listIter) Error() error { @@ -672,7 +616,7 @@ func (b *backend) listLatest(ctx context.Context, req *resource.ListRequest, cb return 0, fmt.Errorf("only works for the 'latest' resource version") } - iter := &listIter{} + iter := &listIter{sortAsc: false} err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { var err error iter.listRV, err = fetchLatestRV(ctx, tx, b.dialect, req.Options.Key.Group, req.Options.Key.Resource) @@ -706,8 +650,11 @@ func (b *backend) listLatest(ctx context.Context, req *resource.ListRequest, cb // listAtRevision fetches the resources from the resource_history table at a specific revision. func (b *backend) listAtRevision(ctx context.Context, req *resource.ListRequest, cb func(resource.ListIterator) error) (int64, error) { + ctx, span := b.tracer.Start(ctx, tracePrefix+"listAtRevision") + defer span.End() + // Get the RV - iter := &listIter{listRV: req.ResourceVersion} + iter := &listIter{listRV: req.ResourceVersion, sortAsc: false} if req.NextPageToken != "" { continueToken, err := resource.GetContinueToken(req.NextPageToken) if err != nil { @@ -724,6 +671,10 @@ func (b *backend) listAtRevision(ctx context.Context, req *resource.ListRequest, return 0, apierrors.NewBadRequest("expecting an explicit resource version query") } + // The query below has the potential to be EXTREMELY slow if the resource_history table is big. May be helpful to know + // which stack is calling this. + b.log.Debug("listAtRevision", "ns", req.Options.Key.Namespace, "group", req.Options.Key.Group, "resource", req.Options.Key.Resource, "rv", iter.listRV) + err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { limit := int64(0) // ignore limit if iter.offset > 0 { @@ -757,14 +708,37 @@ func (b *backend) listAtRevision(ctx context.Context, req *resource.ListRequest, return iter.listRV, err } -// listLatest fetches the resources from the resource table. +// getHistory fetches the resources from the resource table. func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb func(resource.ListIterator) error) (int64, error) { + // Backwards compatibility for ResourceVersionMatch + if req.VersionMatch != nil && req.GetVersionMatchV2() == resource.ResourceVersionMatchV2_UNKNOWN { + switch req.GetVersionMatch() { + case resource.ResourceVersionMatch_DEPRECATED_NotOlderThan: + // This is not a typo. The old implementation actually did behave like Unset. + req.VersionMatchV2 = resource.ResourceVersionMatchV2_Unset + case resource.ResourceVersionMatch_DEPRECATED_Exact: + req.VersionMatchV2 = resource.ResourceVersionMatchV2_Exact + default: + return 0, fmt.Errorf("unknown version match: %v", req.GetVersionMatch()) + } + + // Log the migration for debugging purposes + b.log.Debug("Old client request received, migrating from version_match to version_match_v2", + "oldValue", req.GetVersionMatch(), + "newValue", req.GetVersionMatchV2()) + } + listReq := sqlGetHistoryRequest{ SQLTemplate: sqltemplate.New(b.dialect), Key: req.Options.Key, Trash: req.Source == resource.ListRequest_TRASH, } + // We are assuming that users want history in ascending order + // when they are using NotOlderThan matching, and descending order + // for Unset (default) and Exact matching. + listReq.SortAscending = req.GetVersionMatchV2() == resource.ResourceVersionMatchV2_NotOlderThan + iter := &listIter{} if req.NextPageToken != "" { continueToken, err := resource.GetContinueToken(req.NextPageToken) @@ -772,6 +746,21 @@ func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb return 0, fmt.Errorf("get continue token: %w", err) } listReq.StartRV = continueToken.ResourceVersion + listReq.SortAscending = continueToken.SortAscending + } + iter.sortAsc = listReq.SortAscending + + // Set ExactRV when using Exact matching + if req.VersionMatchV2 == resource.ResourceVersionMatchV2_Exact { + if req.ResourceVersion <= 0 { + return 0, fmt.Errorf("expecting an explicit resource version query when using Exact matching") + } + listReq.ExactRV = req.ResourceVersion + } + + // Set MinRV when using NotOlderThan matching to filter at the database level + if req.ResourceVersion > 0 && req.VersionMatchV2 == resource.ResourceVersionMatchV2_NotOlderThan { + listReq.MinRV = req.ResourceVersion } err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index d2ef981e91b..f275b2b78e5 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql/driver" "errors" + "fmt" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" @@ -375,84 +376,291 @@ func TestBackend_delete(t *testing.T) { }) } -func TestBackend_restore(t *testing.T) { +func TestBackend_getHistory(t *testing.T) { t.Parallel() - meta, err := utils.MetaAccessor(&unstructured.Unstructured{ - Object: map[string]any{}, - }) - require.NoError(t, err) - meta.SetUID("new-uid") - oldMeta, err := utils.MetaAccessor(&unstructured.Unstructured{ - Object: map[string]any{}, - }) - require.NoError(t, err) - oldMeta.SetUID("old-uid") - event := resource.WriteEvent{ - Type: resource.WatchEvent_ADDED, - Key: resKey, - Object: meta, - ObjectOld: oldMeta, + + // Common setup + key := &resource.ResourceKey{ + Namespace: "ns", + Group: "gr", + Resource: "rs", + Name: "nm", + } + rv1, rv2, rv3 := int64(100), int64(200), int64(300) + cols := []string{"resource_version", "namespace", "name", "folder", "value"} + + tests := []struct { + name string + versionMatch resource.ResourceVersionMatchV2 + resourceVersion int64 + expectedVersions []int64 + expectedListRv int64 + expectedRowsCount int + expectedErr string + }{ + { + name: "with ResourceVersionMatch_NotOlderThan", + versionMatch: resource.ResourceVersionMatchV2_NotOlderThan, + resourceVersion: rv2, + expectedVersions: []int64{rv2, rv3}, // Should be in ASC order due to NotOlderThan + expectedListRv: rv3, + expectedRowsCount: 2, + }, + { + name: "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0", + versionMatch: resource.ResourceVersionMatchV2_NotOlderThan, + resourceVersion: 0, + expectedVersions: []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan + expectedListRv: rv3, + expectedRowsCount: 3, + }, + { + name: "with ResourceVersionMatch_Exact", + versionMatch: resource.ResourceVersionMatchV2_Exact, + resourceVersion: rv2, + expectedVersions: []int64{rv2}, + expectedListRv: rv3, + expectedRowsCount: 1, + }, + { + name: "with ResourceVersionMatch_Unset (default)", + expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default + expectedListRv: rv3, + expectedRowsCount: 3, + }, + { + name: "error with ResourceVersionMatch_Exact and ResourceVersion <= 0", + versionMatch: resource.ResourceVersionMatchV2_Exact, + resourceVersion: 0, + expectedErr: "expecting an explicit resource version query when using Exact matching", + }, } - t.Run("happy path", func(t *testing.T) { - t.Parallel() + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + b, ctx := setupBackendTest(t) + + // Build request with appropriate matcher + req := &resource.ListRequest{ + Options: &resource.ListOptions{Key: key}, + ResourceVersion: tc.resourceVersion, + VersionMatchV2: tc.versionMatch, + Source: resource.ListRequest_HISTORY, + } + + // Set up mock expectations only if we don't expect an error + if tc.expectedErr == "" { + // Build expected values map + expectedValues := make(map[int64]string) + for _, rv := range tc.expectedVersions { + expectedValues[rv] = fmt.Sprintf("rv-%d", rv) + } + + // Callback that tracks returned items + callback := func(iter resource.ListIterator) error { + count := 0 + var seenVersions []int64 + for iter.Next() { + count++ + currentRV := iter.ResourceVersion() + seenVersions = append(seenVersions, currentRV) + expectedValue, ok := expectedValues[currentRV] + require.True(t, ok, "Got unexpected RV: %d", currentRV) + require.Equal(t, expectedValue, string(iter.Value())) + } + require.Equal(t, tc.expectedRowsCount, count) + // Verify the order matches what we expect + require.Equal(t, tc.expectedVersions, seenVersions, "Resource versions returned in incorrect order") + return nil + } + + b.SQLMock.ExpectBegin() + + // Expect fetch latest RV call + latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}). + AddRow(rv3, 0) + b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows) + + // Expect history query + historyRows := sqlmock.NewRows(cols) + for _, rv := range tc.expectedVersions { + historyRows.AddRow( + rv, // resource_version + "ns", // namespace + "nm", // name + "folder", // folder + []byte(fmt.Sprintf("rv-%d", rv)), // value + ) + } + b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows) + b.SQLMock.ExpectCommit() + + // Execute the test + listRv, err := b.getHistory(ctx, req, callback) + require.NoError(t, err) + require.Equal(t, tc.expectedListRv, listRv) + } else { + // For error cases, we use a simple empty callback + callback := func(iter resource.ListIterator) error { return nil } + + // Execute the test expecting an error + listRv, err := b.getHistory(ctx, req, callback) + require.Zero(t, listRv) + require.Error(t, err) + require.ErrorContains(t, err, tc.expectedErr) + } + }) + } +} + +// TestBackend_getHistoryPagination tests the ordering behavior for ResourceVersionMatch_NotOlderThan +// when using pagination, ensuring entries are returned in oldest-to-newest order. +func TestBackend_getHistoryPagination(t *testing.T) { + t.Parallel() + + // Common setup + key := &resource.ResourceKey{ + Namespace: "ns", + Group: "gr", + Resource: "rs", + Name: "nm", + } + + // Create resource versions that will be returned in our test + versions := make([]int64, 10) + for i := range versions { + versions[i] = int64(51 + i) + } + rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60 := versions[0], versions[1], versions[2], versions[3], versions[4], versions[5], versions[6], versions[7], versions[8], versions[9] + + t.Run("pagination with NotOlderThan should return entries from oldest to newest", func(t *testing.T) { b, ctx := setupBackendTest(t) + // Define all pages we want to test + pages := []struct { + versions []int64 + token *resource.ContinueToken + }{ + { + versions: []int64{rv51, rv52, rv53, rv54}, + token: nil, + }, + { + versions: []int64{rv55, rv56, rv57, rv58}, + token: &resource.ContinueToken{ + ResourceVersion: rv54, + StartOffset: 4, + SortAscending: true, + }, + }, + { + versions: []int64{rv59, rv60}, + token: &resource.ContinueToken{ + ResourceVersion: rv58, + StartOffset: 8, + SortAscending: true, + }, + }, + } + + var allItems []int64 + initialRV := rv51 + + // Test each page + for _, page := range pages { + req := &resource.ListRequest{ + Options: &resource.ListOptions{Key: key}, + ResourceVersion: initialRV, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Source: resource.ListRequest_HISTORY, + Limit: 4, + } + if page.token != nil { + req.NextPageToken = page.token.String() + } + + items := make([]int64, 0) + callback := func(iter resource.ListIterator) error { + for iter.Next() { + items = append(items, iter.ResourceVersion()) + } + return nil + } + + b.SQLMock.ExpectBegin() + historyRows := setupHistoryTest(b, page.versions, rv60) + b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows) + b.SQLMock.ExpectCommit() + + listRv, err := b.getHistory(ctx, req, callback) + require.NoError(t, err) + require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)") + require.Equal(t, page.versions, items, "Items should be in ASC order") + + allItems = append(allItems, items...) + } + + // Verify complete sequence + expectedAllItems := []int64{rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60} + require.Equal(t, expectedAllItems, allItems) + }) + + t.Run("pagination with ResourceVersion=0 and NotOlderThan should return entries in ASC order", func(t *testing.T) { + b, ctx := setupBackendTest(t) + + req := &resource.ListRequest{ + Options: &resource.ListOptions{Key: key}, + ResourceVersion: 0, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Source: resource.ListRequest_HISTORY, + Limit: 4, + } + + // First batch of items we expect, in ASC order (because of NotOlderThan flag) + // Even with ResourceVersion=0, the order is ASC because we use SortAscending=true + expectedVersions := []int64{rv51, rv52, rv53, rv54} + items := make([]int64, 0) + + callback := func(iter resource.ListIterator) error { + for iter.Next() { + items = append(items, iter.ResourceVersion()) + } + return nil + } + b.SQLMock.ExpectBegin() - expectSuccessfulResourceVersionExec(t, b.TestDBProvider, - func() { b.ExecWithResult("insert resource", 0, 1) }, - func() { b.ExecWithResult("insert resource_history", 0, 1) }, - func() { b.ExecWithResult("update resource_history", 0, 1) }, - ) + historyRows := setupHistoryTest(b, expectedVersions, rv60) + b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows) b.SQLMock.ExpectCommit() - v, err := b.restore(ctx, event) + listRv, err := b.getHistory(ctx, req, callback) require.NoError(t, err) - require.Equal(t, int64(200), v) - }) - - t.Run("error restoring resource", func(t *testing.T) { - t.Parallel() - b, ctx := setupBackendTest(t) - - b.SQLMock.ExpectBegin() - b.ExecWithErr("insert resource", errTest) - b.SQLMock.ExpectRollback() - - v, err := b.restore(ctx, event) - require.Zero(t, v) - require.Error(t, err) - require.ErrorContains(t, err, "insert into resource") - }) - - t.Run("error inserting into resource history", func(t *testing.T) { - t.Parallel() - b, ctx := setupBackendTest(t) - - b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource", 0, 1) - b.ExecWithErr("insert resource_history", errTest) - b.SQLMock.ExpectRollback() - - v, err := b.restore(ctx, event) - require.Zero(t, v) - require.Error(t, err) - require.ErrorContains(t, err, "insert into resource history") - }) - - t.Run("error updating resource history uid", func(t *testing.T) { - t.Parallel() - b, ctx := setupBackendTest(t) - - b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource", 0, 1) - b.ExecWithResult("insert resource_history", 0, 1) - b.ExecWithErr("update resource_history", errTest) - b.SQLMock.ExpectRollback() - - v, err := b.restore(ctx, event) - require.Zero(t, v) - require.Error(t, err) - require.ErrorContains(t, err, "update history uid") + require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)") + require.Equal(t, expectedVersions, items, "Items should be in ASC order even with ResourceVersion=0") }) } + +// setupHistoryTest creates the necessary mock expectations for a history test +func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64) *sqlmock.Rows { + // Expect fetch latest RV call - set to the highest resource version + latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}). + AddRow(latestRV, 0) + b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows) + + // Create the mock rows for the history items + cols := []string{"resource_version", "namespace", "name", "folder", "value"} + historyRows := sqlmock.NewRows(cols) + for _, rv := range resourceVersions { + historyRows.AddRow( + rv, // resource_version + "ns", // namespace + "nm", // name + "folder", // folder + []byte(fmt.Sprintf("rv-%d", rv)), // value + ) + } + + return historyRows +} diff --git a/pkg/storage/unified/sql/data/resource_history_get.sql b/pkg/storage/unified/sql/data/resource_history_get.sql index 5ab25eb2e0c..379cc67967c 100644 --- a/pkg/storage/unified/sql/data/resource_history_get.sql +++ b/pkg/storage/unified/sql/data/resource_history_get.sql @@ -16,6 +16,20 @@ WHERE 1 = 1 AND {{ .Ident "action" }} = 3 {{ end }} {{ if (gt .StartRV 0) }} + {{ if .SortAscending }} + AND {{ .Ident "resource_version" }} > {{ .Arg .StartRV }} + {{ else }} AND {{ .Ident "resource_version" }} < {{ .Arg .StartRV }} {{ end }} + {{ end }} + {{ if (gt .MinRV 0) }} + AND {{ .Ident "resource_version" }} >= {{ .Arg .MinRV }} + {{ end }} + {{ if (gt .ExactRV 0) }} + AND {{ .Ident "resource_version" }} = {{ .Arg .ExactRV }} + {{ end }} +{{ if .SortAscending }} +ORDER BY resource_version ASC +{{ else }} ORDER BY resource_version DESC +{{ end }} diff --git a/pkg/storage/unified/sql/data/resource_history_prune.sql b/pkg/storage/unified/sql/data/resource_history_prune.sql index e636e9dc73c..4bff037876a 100644 --- a/pkg/storage/unified/sql/data/resource_history_prune.sql +++ b/pkg/storage/unified/sql/data/resource_history_prune.sql @@ -17,6 +17,7 @@ WHERE {{ .Ident "guid" }} IN ( {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }} AND {{ .Ident "group" }} = {{ .Arg .Key.Group }} AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }} + AND {{ .Ident "name" }} = {{ .Arg .Key.Name }} ) AS {{ .Ident "ranked" }} WHERE {{ .Ident "rn" }} > {{ .Arg .HistoryLimit }} ); diff --git a/pkg/storage/unified/sql/data/resource_history_read.sql b/pkg/storage/unified/sql/data/resource_history_read.sql index d78209817b2..3eb350e275d 100644 --- a/pkg/storage/unified/sql/data/resource_history_read.sql +++ b/pkg/storage/unified/sql/data/resource_history_read.sql @@ -14,12 +14,8 @@ SELECT AND {{ .Ident "group" }} = {{ .Arg .Request.Key.Group }} AND {{ .Ident "resource" }} = {{ .Arg .Request.Key.Resource }} AND {{ .Ident "name" }} = {{ .Arg .Request.Key.Name }} - {{ if .Request.IncludeDeleted }} - AND {{ .Ident "action" }} != 3 - AND {{ .Ident "value" }} NOT LIKE '%deletionTimestamp%' - {{ end }} {{ if gt .Request.ResourceVersion 0 }} - AND {{ .Ident "resource_version" }} {{ if .Request.IncludeDeleted }}={{ else }}<={{ end }} {{ .Arg .Request.ResourceVersion }} + AND {{ .Ident "resource_version" }} <= {{ .Arg .Request.ResourceVersion }} {{ end }} ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1 diff --git a/pkg/storage/unified/sql/data/resource_history_update_uid.sql b/pkg/storage/unified/sql/data/resource_history_update_uid.sql deleted file mode 100644 index 7624b5e9c53..00000000000 --- a/pkg/storage/unified/sql/data/resource_history_update_uid.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE {{ .Ident "resource_history" }} - SET {{ .Ident "value" }} = REPLACE({{ .Ident "value" }}, CONCAT('"uid":"', {{ .Arg .OldUID }}, '"'), CONCAT('"uid":"', {{ .Arg .NewUID }}, '"')) - WHERE {{ .Ident "name" }} = {{ .Arg .WriteEvent.Key.Name }} - AND {{ .Ident "namespace" }} = {{ .Arg .WriteEvent.Key.Namespace }} - AND {{ .Ident "group" }} = {{ .Arg .WriteEvent.Key.Group }} - AND {{ .Ident "resource" }} = {{ .Arg .WriteEvent.Key.Resource }} - AND {{ .Ident "action" }} != 3 - AND {{ .Ident "value" }} NOT LIKE '%deletionTimestamp%'; diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go index 529cd163eb4..b4300899128 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -11,6 +11,8 @@ import ( "xorm.io/xorm" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/sql/db" ) @@ -18,6 +20,31 @@ import ( // driver. const tlsConfigName = "db_engine_tls" +func getEngine(cfg *setting.Cfg) (*xorm.Engine, error) { + dbSection := cfg.SectionWithEnvOverrides("database") + dbType := dbSection.Key("type").String() + if dbType == "" { + return nil, fmt.Errorf("no database type specified") + } + + switch dbType { + case dbTypeMySQL, dbTypePostgres, dbTypeSQLite: + config, err := sqlstore.NewDatabaseConfig(cfg, nil) + if err != nil { + return nil, nil + } + + engine, err := xorm.NewEngine(dbType, config.ConnectionString) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + return engine, nil + default: + return nil, fmt.Errorf("unsupported database type: %s", dbType) + } +} + +// Deprecated: use getEngine instead func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { config := mysql.NewConfig() config.User = getter.String("user") @@ -108,6 +135,7 @@ func configureTLS(getter confGetter, config *mysql.Config) error { return nil } +// Deprecated: use getEngine instead func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { dsnKV := map[string]string{ "user": getter.String("user"), diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go index 8b39100ffb3..d85260b161a 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -29,6 +30,64 @@ func newValidMySQLGetter(withKeyPrefix bool) confGetter { }, prefix) } +func TestGetEngine(t *testing.T) { + t.Parallel() + + t.Run("MySQL engine", func(t *testing.T) { + t.Parallel() + cfg := setting.NewCfg() + dbSection := cfg.SectionWithEnvOverrides("database") + dbSection.Key("type").SetValue(dbTypeMySQL) + dbSection.Key("host").SetValue("/var/run/mysql.socket") + dbSection.Key("name").SetValue("grafana") + dbSection.Key("user").SetValue("user") + dbSection.Key("password").SetValue("password") + + engine, err := getEngine(cfg) + require.NoError(t, err) + require.NotNil(t, engine) + }) + + t.Run("Postgres engine", func(t *testing.T) { + t.Parallel() + cfg := setting.NewCfg() + dbSection := cfg.SectionWithEnvOverrides("database") + dbSection.Key("type").SetValue(dbTypePostgres) + dbSection.Key("host").SetValue("localhost") + dbSection.Key("name").SetValue("grafana") + dbSection.Key("user").SetValue("user") + dbSection.Key("password").SetValue("password") + + engine, err := getEngine(cfg) + require.NoError(t, err) + require.NotNil(t, engine) + }) + + t.Run("SQLite engine", func(t *testing.T) { + t.Parallel() + cfg := setting.NewCfg() + dbSection := cfg.SectionWithEnvOverrides("database") + dbSection.Key("type").SetValue(dbTypeSQLite) + dbSection.Key("path").SetValue(":memory:") + + engine, err := getEngine(cfg) + require.NoError(t, err) + require.NotNil(t, engine) + }) + + t.Run("Unknown database type", func(t *testing.T) { + t.Parallel() + cfg := setting.NewCfg() + dbSection := cfg.SectionWithEnvOverrides("database") + dbSection.Key("type").SetValue("unknown") + + engine, err := getEngine(cfg) + require.Error(t, err) + require.Nil(t, engine) + require.Contains(t, err.Error(), "unsupported database type") + }) +} + func TestGetEngineMySQLFromConfig(t *testing.T) { t.Parallel() diff --git a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go index 13e6b7fbe42..98fb6f59af0 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go @@ -23,6 +23,7 @@ import ( const ( dbTypeMySQL = "mysql" dbTypePostgres = "postgres" + dbTypeSQLite = "sqlite3" ) const grafanaDBInstrumentQueriesKey = "instrument_queries" @@ -88,8 +89,7 @@ func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace. dbType := getter.String("type") grafanaDBType := fallbackGetter.String("type") switch { - // First try with the config in the "resource_api" section, which is - // specific to Unified Storage + // Deprecated: First try with the config in the "resource_api" section, which is specific to Unified Storage case dbType == dbTypePostgres: p.registerMetrics = true p.engine, err = getEnginePostgres(getter) @@ -100,37 +100,23 @@ func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace. p.engine, err = getEngineMySQL(getter) return p, err - // TODO: add support for SQLite - case dbType != "": return p, fmt.Errorf("invalid db type specified: %s", dbType) - // If we have an empty Resource API db config, try with the core Grafana - // database config - - case grafanaDBType == dbTypePostgres: + // If we have an empty Resource API db config, try with the core Grafana database config + case grafanaDBType != "": p.registerMetrics = true - p.engine, err = getEnginePostgres(fallbackGetter) + p.engine, err = getEngine(cfg) return p, err - - case grafanaDBType == dbTypeMySQL: - p.registerMetrics = true - p.engine, err = getEngineMySQL(fallbackGetter) - return p, err - - // TODO: add support for SQLite - case grafanaDB != nil: - // try to use the grafana db connection - + // try to use the grafana db connection (should only happen in tests) if fallbackGetter.Bool(grafanaDBInstrumentQueriesKey) { return nil, errGrafanaDBInstrumentedNotSupported } p.engine = grafanaDB.GetEngine() return p, nil - default: - return p, fmt.Errorf("no db connection provided") + return p, fmt.Errorf("no database type specified") } } diff --git a/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go b/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go index 38b2f8dc89e..dfbcd93aea7 100644 --- a/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -104,7 +105,8 @@ func TestReproIncident2144UsingGrafanaDB(t *testing.T) { t.Run("Resource API provides a reasonable error for this case", func(t *testing.T) { t.Parallel() - cfg := newCfgFromIniMap(t, cfgMap) + cfg := setting.NewCfg() + cfg.SectionWithEnvOverrides("database").Key(grafanaDBInstrumentQueriesKey).SetValue("true") resourceDB, err := ProvideResourceDB(grafanaDB, cfg, nil) require.Nil(t, resourceDB) require.Error(t, err) diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index 3ea34472024..0da9cd9187e 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -39,7 +39,6 @@ var ( sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql") sqlResourceHistoryRead = mustTemplate("resource_history_read.sql") sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql") - sqlResoureceHistoryUpdateUid = mustTemplate("resource_history_update_uid.sql") sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql") sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql") sqlResourceHistoryGet = mustTemplate("resource_history_get.sql") @@ -244,9 +243,12 @@ func (r *sqlResourceHistoryDeleteRequest) Validate() error { type sqlGetHistoryRequest struct { sqltemplate.SQLTemplate - Key *resource.ResourceKey - Trash bool // only deleted items - StartRV int64 // from NextPageToken + Key *resource.ResourceKey + Trash bool // only deleted items + StartRV int64 // from NextPageToken + MinRV int64 // minimum resource version for NotOlderThan + ExactRV int64 // exact resource version for Exact + SortAscending bool // if true, sort by resource_version ASC, otherwise DESC } func (r sqlGetHistoryRequest) Validate() error { @@ -279,19 +281,6 @@ func (r *sqlPruneHistoryRequest) Validate() error { return nil } -// update resource history - -type sqlResourceHistoryUpdateRequest struct { - sqltemplate.SQLTemplate - WriteEvent resource.WriteEvent - OldUID string - NewUID string -} - -func (r sqlResourceHistoryUpdateRequest) Validate() error { - return nil // TODO -} - type sqlResourceBlobInsertRequest struct { sqltemplate.SQLTemplate Now time.Time diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index 4dfb7e13327..6321604fbe4 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -176,26 +176,6 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, - sqlResoureceHistoryUpdateUid: { - { - Name: "modify uids in history", - Data: &sqlResourceHistoryUpdateRequest{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - WriteEvent: resource.WriteEvent{ - Key: &resource.ResourceKey{ - Namespace: "nn", - Group: "gg", - Resource: "rr", - Name: "name", - }, - PreviousRV: 1234, - }, - OldUID: "old-uid", - NewUID: "new-uid", - }, - }, - }, - sqlResourceHistoryInsert: { { Name: "insert into resource_history", @@ -264,6 +244,7 @@ func TestUnifiedStorageQueries(t *testing.T) { Namespace: "nn", Group: "gg", Resource: "rr", + Name: "na", }, HistoryLimit: 100, }, diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index cf62ab0b0d7..8afaf50bacd 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -31,6 +31,10 @@ func TestMain(m *testing.M) { // TestStorageBackend is a test for the StorageBackend interface. func TestIntegrationSQLStorageBackend(t *testing.T) { + if infraDB.IsTestDBSpanner() { + t.Skip("skipping integration test") + } + t.Run("IsHA (polling notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { dbstore := infraDB.InitTestDB(t) @@ -74,6 +78,10 @@ func TestClientServer(t *testing.T) { if infraDB.IsTestDbSQLite() { t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") } + if infraDB.IsTestDBSpanner() { + t.Skip("skipping integration test") + } + ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) dbstore := infraDB.InitTestDB(t) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_prune-simple.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_prune-simple.sql index fa50d446b4e..2c54050b2d2 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_prune-simple.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_prune-simple.sql @@ -17,6 +17,7 @@ WHERE `guid` IN ( `namespace` = 'nn' AND `group` = 'gg' AND `resource` = 'rr' + AND `name` = 'na' ) AS `ranked` WHERE `rn` > 100 ); diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_uid-modify uids in history.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_update_uid-modify uids in history.sql deleted file mode 100755 index 05431eea218..00000000000 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_uid-modify uids in history.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE `resource_history` - SET `value` = REPLACE(`value`, CONCAT('"uid":"', 'old-uid', '"'), CONCAT('"uid":"', 'new-uid', '"')) - WHERE `name` = 'name' - AND `namespace` = 'nn' - AND `group` = 'gg' - AND `resource` = 'rr' - AND `action` != 3 - AND `value` NOT LIKE '%deletionTimestamp%'; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_prune-simple.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_prune-simple.sql index 9994708de39..08a36ac095a 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_prune-simple.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_prune-simple.sql @@ -17,6 +17,7 @@ WHERE "guid" IN ( "namespace" = 'nn' AND "group" = 'gg' AND "resource" = 'rr' + AND "name" = 'na' ) AS "ranked" WHERE "rn" > 100 ); diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_uid-modify uids in history.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_update_uid-modify uids in history.sql deleted file mode 100755 index 79192fa5a6e..00000000000 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_uid-modify uids in history.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE "resource_history" - SET "value" = REPLACE("value", CONCAT('"uid":"', 'old-uid', '"'), CONCAT('"uid":"', 'new-uid', '"')) - WHERE "name" = 'name' - AND "namespace" = 'nn' - AND "group" = 'gg' - AND "resource" = 'rr' - AND "action" != 3 - AND "value" NOT LIKE '%deletionTimestamp%'; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_prune-simple.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_prune-simple.sql index 9994708de39..08a36ac095a 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_prune-simple.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_prune-simple.sql @@ -17,6 +17,7 @@ WHERE "guid" IN ( "namespace" = 'nn' AND "group" = 'gg' AND "resource" = 'rr' + AND "name" = 'na' ) AS "ranked" WHERE "rn" > 100 ); diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_uid-modify uids in history.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_uid-modify uids in history.sql deleted file mode 100755 index 79192fa5a6e..00000000000 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_uid-modify uids in history.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE "resource_history" - SET "value" = REPLACE("value", CONCAT('"uid":"', 'old-uid', '"'), CONCAT('"uid":"', 'new-uid', '"')) - WHERE "name" = 'name' - AND "namespace" = 'nn' - AND "group" = 'gg' - AND "resource" = 'rr' - AND "action" != 3 - AND "value" NOT LIKE '%deletionTimestamp%'; diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index 70438f91ebb..c45d13ce6ba 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -2,12 +2,14 @@ package test import ( "context" + "fmt" "slices" "strings" "testing" "time" "github.com/go-jose/go-jose/v3/jwt" + "github.com/google/uuid" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -33,7 +35,13 @@ type NewBackendFunc func(ctx context.Context) resource.StorageBackend // TestOptions configures which tests to run type TestOptions struct { SkipTests map[string]bool // tests to skip + NSPrefix string // namespace prefix for isolation +} +// GenerateRandomNSPrefix creates a random namespace prefix for test isolation +func GenerateRandomNSPrefix() string { + uid := uuid.New().String()[:10] + return fmt.Sprintf("test-%s", uid) } // RunStorageBackendTest runs the storage backend test suite @@ -46,9 +54,15 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp opts = &TestOptions{} } + if opts.NSPrefix == "" { + opts.NSPrefix = GenerateRandomNSPrefix() + } + + t.Logf("Running tests with namespace prefix: %s", opts.NSPrefix) + cases := []struct { name string - fn func(*testing.T, resource.StorageBackend) + fn func(*testing.T, resource.StorageBackend, string) }{ {TestHappyPath, runTestIntegrationBackendHappyPath}, {TestWatchWriteEvents, runTestIntegrationBackendWatchWriteEvents}, @@ -65,12 +79,12 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp } t.Run(tc.name, func(t *testing.T) { - tc.fn(t, newBackend(context.Background())) + tc.fn(t, newBackend(context.Background()), opts.NSPrefix) }) } } -func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ Subject: "testuser", @@ -79,39 +93,46 @@ func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBa })) server := newServer(t, backend) - + ns := nsPrefix + "-ns1" stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout require.NoError(t, err) var rv1, rv2, rv3, rv4, rv5 int64 t.Run("Add 3 resources", func(t *testing.T) { - rv1, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + rv1, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv1, int64(0)) - rv2, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + rv2, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv2, rv1) - rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) + rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv3, rv2) }) t.Run("Update item2", func(t *testing.T) { - rv4, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + rv4, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv4, rv3) }) t.Run("Delete item1", func(t *testing.T) { - rv5, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED) + rv5, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv5, rv4) }) t.Run("Read latest item 2", func(t *testing.T) { - resp := backend.ReadResource(ctx, &resource.ReadRequest{Key: resourceKey("item2")}) + resp := backend.ReadResource(ctx, &resource.ReadRequest{ + Key: &resource.ResourceKey{ + Name: "item2", + Namespace: ns, + Group: "group", + Resource: "resource", + }, + }) require.Nil(t, resp.Error) require.Equal(t, rv4, resp.ResourceVersion) require.Equal(t, "item2 MODIFIED", string(resp.Value)) @@ -120,7 +141,12 @@ func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBa t.Run("Read early version of item2", func(t *testing.T) { resp := backend.ReadResource(ctx, &resource.ReadRequest{ - Key: resourceKey("item2"), + Key: &resource.ResourceKey{ + Name: "item2", + Namespace: ns, + Group: "group", + Resource: "resource", + }, ResourceVersion: rv3, // item2 was created at rv2 and updated at rv4 }) require.Nil(t, resp.Error) @@ -132,7 +158,7 @@ func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBa resp, err := server.List(ctx, &resource.ListRequest{ Options: &resource.ListOptions{ Key: &resource.ResourceKey{ - Namespace: "namespace", + Namespace: ns, Group: "group", Resource: "resource", }, @@ -176,7 +202,7 @@ func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBa }) } -func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) sortFunc := func(a, b resource.ResourceStats) int { @@ -190,37 +216,37 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St } // Create resources across different namespaces/groups _, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, - WithNamespace("ns1"), + WithNamespace(nsPrefix+"-ns1"), WithGroup("group"), WithResource("resource1")) require.NoError(t, err) _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED, - WithNamespace("ns1"), + WithNamespace(nsPrefix+"-ns1"), WithGroup("group"), WithResource("resource1")) require.NoError(t, err) _, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED, - WithNamespace("ns1"), + WithNamespace(nsPrefix+"-ns1"), WithGroup("group"), WithResource("resource2")) require.NoError(t, err) _, err = writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED, - WithNamespace("ns2"), + WithNamespace(nsPrefix+"-ns2"), WithGroup("group"), WithResource("resource1")) require.NoError(t, err) _, err = writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED, - WithNamespace("ns2"), + WithNamespace(nsPrefix+"-ns2"), WithGroup("group"), WithResource("resource1")) require.NoError(t, err) t.Run("Get stats for ns1", func(t *testing.T) { - stats, err := backend.GetResourceStats(ctx, "ns1", 0) + stats, err := backend.GetResourceStats(ctx, nsPrefix+"-ns1", 0) require.NoError(t, err) require.Len(t, stats, 2) @@ -228,14 +254,14 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St slices.SortFunc(stats, sortFunc) // Check first resource stats - require.Equal(t, "ns1", stats[0].Namespace) + require.Equal(t, nsPrefix+"-ns1", stats[0].Namespace) require.Equal(t, "group", stats[0].Group) require.Equal(t, "resource1", stats[0].Resource) require.Equal(t, int64(2), stats[0].Count) require.Greater(t, stats[0].ResourceVersion, int64(0)) // Check second resource stats - require.Equal(t, "ns1", stats[1].Namespace) + require.Equal(t, nsPrefix+"-ns1", stats[1].Namespace) require.Equal(t, "group", stats[1].Group) require.Equal(t, "resource2", stats[1].Resource) require.Equal(t, int64(1), stats[1].Count) @@ -243,11 +269,11 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St }) t.Run("Get stats for ns2", func(t *testing.T) { - stats, err := backend.GetResourceStats(ctx, "ns2", 0) + stats, err := backend.GetResourceStats(ctx, nsPrefix+"-ns2", 0) require.NoError(t, err) require.Len(t, stats, 1) - require.Equal(t, "ns2", stats[0].Namespace) + require.Equal(t, nsPrefix+"-ns2", stats[0].Namespace) require.Equal(t, "group", stats[0].Group) require.Equal(t, "resource1", stats[0].Resource) require.Equal(t, int64(2), stats[0].Count) @@ -255,11 +281,11 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St }) t.Run("Get stats with minimum count", func(t *testing.T) { - stats, err := backend.GetResourceStats(ctx, "ns1", 1) + stats, err := backend.GetResourceStats(ctx, nsPrefix+"-ns1", 1) require.NoError(t, err) require.Len(t, stats, 1) - require.Equal(t, "ns1", stats[0].Namespace) + require.Equal(t, nsPrefix+"-ns1", stats[0].Namespace) require.Equal(t, "group", stats[0].Group) require.Equal(t, "resource1", stats[0].Resource) require.Equal(t, int64(2), stats[0].Count) @@ -272,11 +298,11 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St }) } -func runTestIntegrationBackendWatchWriteEvents(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendWatchWriteEvents(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) // Create a few resources before initing the watch - _, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + _, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, WithNamespace(nsPrefix+"-ns1")) require.NoError(t, err) // Start the watch @@ -284,7 +310,7 @@ func runTestIntegrationBackendWatchWriteEvents(t *testing.T, backend resource.St require.NoError(t, err) // Create one more event - _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED, WithNamespace(nsPrefix+"-ns1")) require.NoError(t, err) require.Equal(t, "item2", (<-stream).Key.Name) @@ -295,33 +321,33 @@ func runTestIntegrationBackendWatchWriteEvents(t *testing.T, backend resource.St require.False(t, ok) } -func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) server := newServer(t, backend) - + ns := nsPrefix + "-ns1" // Create a few resources before starting the watch - rv1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + rv1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv1, int64(0)) - rv2, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + rv2, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv2, rv1) - rv3, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) + rv3, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv3, rv2) - rv4, err := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED) + rv4, err := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv4, rv3) - rv5, err := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED) + rv5, err := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv5, rv4) - rv6, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + rv6, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv6, rv5) - rv7, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED) + rv7, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv7, rv6) - rv8, err := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED) + rv8, err := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rv8, rv7) @@ -417,6 +443,7 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend continueToken := &resource.ContinueToken{ ResourceVersion: rv8, StartOffset: 2, + SortAscending: false, } res, err := server.List(ctx, &resource.ListRequest{ NextPageToken: continueToken.String(), @@ -442,66 +469,166 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend }) } -func runTestIntegrationBackendListHistory(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendListHistory(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) server := newServer(t, backend) - - rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + ns := nsPrefix + "-ns1" + rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, WithNamespace(ns)) require.Greater(t, rv1, int64(0)) // add 5 events for item1 - should be saved to history - rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rvHistory1, rv1) - rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rvHistory2, rvHistory1) - rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rvHistory3, rvHistory2) - rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rvHistory4, rvHistory3) - rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED, WithNamespace(ns)) require.NoError(t, err) require.Greater(t, rvHistory5, rvHistory4) - t.Run("fetch first history page at revision with limit", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - Limit: 3, - Source: resource.ListRequest_HISTORY, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - Name: "item1", - }, - }, - }) - require.NoError(t, err) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 3) - t.Log(res.Items) - // should be in desc order, so the newest RVs are returned first - require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) - require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) - require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) - require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value)) - require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) + t.Run("fetch history with different version matching", func(t *testing.T) { + baseKey := &resource.ResourceKey{ + Namespace: ns, + Group: "group", + Resource: "resource", + Name: "item1", + } - continueToken, err := resource.GetContinueToken(res.NextPageToken) - require.NoError(t, err) - // should return the furthest back RV as the next page token - require.Equal(t, rvHistory3, continueToken.ResourceVersion) + tests := []struct { + name string + request *resource.ListRequest + expectedVersions []int64 + expectedValues []string + minExpectedHeadRV int64 + expectedContinueRV int64 + expectedSortAsc bool + }{ + { + name: "NotOlderThan with rv1 (ASC order)", + request: &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + ResourceVersion: rv1, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Options: &resource.ListOptions{ + Key: baseKey, + }, + }, + expectedVersions: []int64{rv1, rvHistory1, rvHistory2}, + expectedValues: []string{"item1 ADDED", "item1 MODIFIED", "item1 MODIFIED"}, + minExpectedHeadRV: rvHistory2, + expectedContinueRV: rvHistory2, + expectedSortAsc: true, + }, + { + name: "NotOlderThan with rv=0 (ASC order)", + request: &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + ResourceVersion: 0, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Options: &resource.ListOptions{ + Key: baseKey, + }, + }, + expectedVersions: []int64{rv1, rvHistory1, rvHistory2}, + expectedValues: []string{"item1 ADDED", "item1 MODIFIED", "item1 MODIFIED"}, + minExpectedHeadRV: rvHistory2, + expectedContinueRV: rvHistory2, + expectedSortAsc: true, + }, + { + name: "ResourceVersionMatch_Unset (DESC order)", + request: &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: baseKey, + }, + }, + expectedVersions: []int64{rvHistory5, rvHistory4, rvHistory3}, + expectedValues: []string{"item1 MODIFIED", "item1 MODIFIED", "item1 MODIFIED"}, + minExpectedHeadRV: rvHistory5, + expectedContinueRV: rvHistory3, + expectedSortAsc: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := server.List(ctx, tc.request) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + + // Check versions and values match expectations + for i := 0; i < 3; i++ { + require.Equal(t, tc.expectedVersions[i], res.Items[i].ResourceVersion) + require.Equal(t, tc.expectedValues[i], string(res.Items[i].Value)) + } + + // Check resource version in response + require.GreaterOrEqual(t, res.ResourceVersion, tc.minExpectedHeadRV) + + // Check continue token + continueToken, err := resource.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + require.Equal(t, tc.expectedContinueRV, continueToken.ResourceVersion) + require.Equal(t, tc.expectedSortAsc, continueToken.SortAscending) + }) + } + + // Test pagination for NotOlderThan (second page) + t.Run("second page with NotOlderThan", func(t *testing.T) { + // Get first page + firstRequest := &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + ResourceVersion: rv1, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Options: &resource.ListOptions{Key: baseKey}, + } + firstPageRes, err := server.List(ctx, firstRequest) + require.NoError(t, err) + + // Get continue token for second page + continueToken, err := resource.GetContinueToken(firstPageRes.NextPageToken) + require.NoError(t, err) + + // Get second page + secondPageRes, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + ResourceVersion: rv1, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + NextPageToken: continueToken.String(), + Options: &resource.ListOptions{Key: baseKey}, + }) + require.NoError(t, err) + require.Nil(t, secondPageRes.Error) + require.Len(t, secondPageRes.Items, 3) + + // Second page should continue in ascending order + expectedRVs := []int64{rvHistory3, rvHistory4, rvHistory5} + for i, expectedRV := range expectedRVs { + require.Equal(t, expectedRV, secondPageRes.Items[i].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(secondPageRes.Items[i].Value)) + } + }) }) t.Run("fetch second page of history at revision", func(t *testing.T) { continueToken := &resource.ContinueToken{ ResourceVersion: rvHistory3, StartOffset: 2, + SortAscending: false, } res, err := server.List(ctx, &resource.ListRequest{ NextPageToken: continueToken.String(), @@ -509,7 +636,7 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage Source: resource.ListRequest_HISTORY, Options: &resource.ListOptions{ Key: &resource.ResourceKey{ - Namespace: "namespace", + Namespace: ns, Group: "group", Resource: "resource", Name: "item1", @@ -525,17 +652,135 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) require.Equal(t, rvHistory1, res.Items[1].ResourceVersion) }) + + t.Run("paginated history with NotOlderThan returns items in ascending order", func(t *testing.T) { + // Create 10 versions of a resource to test pagination + ns2 := nsPrefix + "-ns2" + resourceKey := &resource.ResourceKey{ + Namespace: ns2, + Group: "group", + Resource: "resource", + Name: "paged-item", + } + + var resourceVersions []int64 + + // First create the initial resource + initialRV, err := writeEvent(ctx, backend, "paged-item", resource.WatchEvent_ADDED, WithNamespace(ns2)) + require.NoError(t, err) + resourceVersions = append(resourceVersions, initialRV) + + // Create 9 more versions with modifications + for i := 0; i < 9; i++ { + rv, err := writeEvent(ctx, backend, "paged-item", resource.WatchEvent_MODIFIED, WithNamespace(ns2)) + require.NoError(t, err) + resourceVersions = append(resourceVersions, rv) + } + + // Now we should have 10 versions total (1 ADDED + 9 MODIFIED) + require.Len(t, resourceVersions, 10) + + // Test pagination with limit of 3 and NotOlderThan, starting from the beginning + pages := []struct { + pageNumber int + pageSize int + startToken string + }{ + {pageNumber: 1, pageSize: 3, startToken: ""}, + {pageNumber: 2, pageSize: 3, startToken: ""}, // Will be set in the test + {pageNumber: 3, pageSize: 3, startToken: ""}, // Will be set in the test + {pageNumber: 4, pageSize: 1, startToken: ""}, // Will be set in the test - last page with remaining item + } + + var allItems []*resource.ResourceWrapper + + // Request first page with NotOlderThan and ResourceVersion=0 (should start from oldest) + for i, page := range pages { + req := &resource.ListRequest{ + Limit: int64(page.pageSize), + Source: resource.ListRequest_HISTORY, + ResourceVersion: 0, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Options: &resource.ListOptions{ + Key: resourceKey, + }, + } + + if i > 0 { + // For subsequent pages, use the continue token from the previous page + req.NextPageToken = pages[i].startToken + } + + res, err := server.List(ctx, req) + require.NoError(t, err) + require.Nil(t, res.Error) + + // First 3 pages should have exactly pageSize items + if i < 3 { + require.Len(t, res.Items, page.pageSize, "Page %d should have %d items", i+1, page.pageSize) + } else { + // Last page should have 1 item (10 items total with 3+3+3+1 distribution) + require.Len(t, res.Items, 1, "Last page should have 1 item") + } + + // Save continue token for next page if not the last page + if i < len(pages)-1 { + pages[i+1].startToken = res.NextPageToken + require.NotEmpty(t, res.NextPageToken, "Should have continue token for page %d", i+1) + } else { + // Last page should not have a continue token + require.Empty(t, res.NextPageToken, "Last page should not have continue token") + } + + // Add items to our collection + allItems = append(allItems, res.Items...) + + // Verify all items in current page are in ascending order + for j := 1; j < len(res.Items); j++ { + require.Less(t, res.Items[j-1].ResourceVersion, res.Items[j].ResourceVersion, + "Items within page %d should be in ascending order", i+1) + } + + // For pages after the first, verify first item of current page is greater than last item of previous page + if i > 0 && len(allItems) > page.pageSize { + prevPageLastIdx := len(allItems) - len(res.Items) - 1 + currentPageFirstIdx := len(allItems) - len(res.Items) + require.Greater(t, allItems[currentPageFirstIdx].ResourceVersion, allItems[prevPageLastIdx].ResourceVersion, + "First item of page %d should have higher RV than last item of page %d", i+1, i) + } + } + + // Verify we got all 10 items + require.Len(t, allItems, 10, "Should have retrieved all 10 items") + + // Verify all items are in ascending order of resource version + for i := 1; i < len(allItems); i++ { + require.Less(t, allItems[i-1].ResourceVersion, allItems[i].ResourceVersion, + "All items should be in ascending order of resource version") + } + + // Verify the first item is the initial ADDED event + require.Equal(t, initialRV, allItems[0].ResourceVersion, "First item should be the initial ADDED event") + require.Equal(t, "paged-item ADDED", string(allItems[0].Value)) + + // Verify all other items are MODIFIED events and correspond to our recorded resource versions + for i := 1; i < len(allItems); i++ { + require.Equal(t, "paged-item MODIFIED", string(allItems[i].Value)) + require.Equal(t, resourceVersions[i], allItems[i].ResourceVersion) + } + }) } -func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) server := newServer(t, backend) store, ok := backend.(resource.BlobSupport) require.True(t, ok) + ns := nsPrefix + "-ns1" t.Run("put and fetch blob", func(t *testing.T) { key := &resource.ResourceKey{ - Namespace: "ns", + Namespace: ns, Group: "g", Resource: "r", Name: "n", @@ -687,15 +932,6 @@ func writeEvent(ctx context.Context, store resource.StorageBackend, name string, }) } -func resourceKey(name string) *resource.ResourceKey { - return &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - Name: name, - } -} - func newServer(t *testing.T, b resource.StorageBackend) resource.ResourceServer { t.Helper() diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index e07382e6bca..df25c2ac8f5 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -100,7 +100,7 @@ var ( ) func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { - runTest := func(t *testing.T, enableLokiPaths bool) { + runTest := func(t *testing.T, enableLokiPaths bool, postContentType string) { testinfra.SQLiteIntegrationTest(t) // Setup Grafana and its Database @@ -136,12 +136,16 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + postContentTypeHeader := map[string]string{ + "Content-Type": postContentType, + } + t.Run("create rule groups and get them back", func(t *testing.T) { - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, postContentTypeHeader) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, postContentTypeHeader) // create a third group in a different namespace - apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, postContentTypeHeader) // And a non-provisioned rule in another namespace namespace3UID := util.GenerateShortUID() @@ -173,11 +177,16 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { requireStatusCode(t, http.StatusForbidden, status, raw) }) + t.Run("with incorrect content-type should receive 415", func(t *testing.T) { + _, status, raw := apiClient.RawConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, map[string]string{"Content-Type": "application/xml"}) + requireStatusCode(t, http.StatusUnsupportedMediaType, status, raw) + }) + t.Run("delete one rule group", func(t *testing.T) { // Create three groups - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) - apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, postContentTypeHeader) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, postContentTypeHeader) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, postContentTypeHeader) // delete the first one apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name, nil) @@ -202,13 +211,51 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { }) } - t.Run("with the mimirtool paths", func(t *testing.T) { - runTest(t, false) - }) + const applicationYAML = "application/yaml" + const applicationJSON = "application/json" - t.Run("with the cortextool Loki paths", func(t *testing.T) { - runTest(t, true) - }) + cases := []struct { + name string + contentType string + enableLokiPaths bool + }{ + { + name: "with the mimirtool paths; empty content-type", + contentType: "", + enableLokiPaths: false, + }, + { + name: "with the cortextool Loki paths; empty content-type", + contentType: "", + enableLokiPaths: true, + }, + { + name: "with the mimirtool paths; yaml", + contentType: applicationYAML, + enableLokiPaths: false, + }, + { + name: "with the cortextool Loki paths; yaml", + contentType: applicationYAML, + enableLokiPaths: true, + }, + { + name: "with the mimirtool paths; json", + contentType: applicationJSON, + enableLokiPaths: false, + }, + { + name: "with the cortextool Loki paths; json", + contentType: applicationJSON, + enableLokiPaths: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + runTest(t, tc.enableLokiPaths, tc.contentType) + }) + } } func TestIntegrationConvertPrometheusEndpoints_UpdateRule(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index a70f4e19321..5d678bbc997 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -1001,28 +1001,20 @@ func TestIntegrationAlertRuleConflictingTitle(t *testing.T) { require.Equal(t, http.StatusAccepted, status) require.Len(t, createdRuleGroup.Rules, 2) - t.Run("trying to create alert with same title under same folder should fail", func(t *testing.T) { + t.Run("trying to create alert with same title under same folder should not fail", func(t *testing.T) { rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup.GettableRuleGroupConfig) rulesWithUID.Rules = append(rulesWithUID.Rules, rules.Rules[0]) // Create new copy of first rule. _, status, body := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID, false) - assert.Equal(t, http.StatusConflict, status) - - var res map[string]any - require.NoError(t, json.Unmarshal([]byte(body), &res)) - require.Contains(t, res["message"], ngmodels.ErrAlertRuleUniqueConstraintViolation.Error()) + requireStatusCode(t, http.StatusAccepted, status, body) }) - t.Run("trying to update an alert to the title of an existing alert in the same folder should fail", func(t *testing.T) { + t.Run("trying to update an alert to the title of an existing alert in the same folder should not fail", func(t *testing.T) { rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup.GettableRuleGroupConfig) rulesWithUID.Rules[1].GrafanaManagedAlert.Title = "AlwaysFiring" _, status, body := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID, false) - assert.Equal(t, http.StatusConflict, status) - - var res map[string]any - require.NoError(t, json.Unmarshal([]byte(body), &res)) - require.Contains(t, res["message"], ngmodels.ErrAlertRuleUniqueConstraintViolation.Error()) + requireStatusCode(t, http.StatusAccepted, status, body) }) t.Run("trying to create alert with same title under another folder should succeed", func(t *testing.T) { diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 429f32e755e..f9bb02df433 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -1145,16 +1145,26 @@ func (a apiClient) RawConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitl path = "%s/api/convert/api/prom/rules/%s" } - data, err := yaml.Marshal(promGroup) - require.NoError(t, err) + // Based on the content-type header, marshal the data to JSON or YAML + contentType := headers["Content-Type"] + var data []byte + var err error + if contentType == "application/json" { + data, err = json.Marshal(promGroup) + require.NoError(t, err) + } else { + data, err = yaml.Marshal(promGroup) + require.NoError(t, err) + } + buf := bytes.NewReader(data) req, err := http.NewRequest(http.MethodPost, fmt.Sprintf(path, a.url, namespaceTitle), buf) require.NoError(t, err) - req.Header.Add("X-Grafana-Alerting-Datasource-UID", datasourceUID) + req.Header.Set("X-Grafana-Alerting-Datasource-UID", datasourceUID) for key, value := range headers { - req.Header.Add(key, value) + req.Header.Set(key, value) } return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 7fab5a77342..00fd4953f41 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -178,8 +178,8 @@ "url": "https://grafana.com/docs/grafana/latest/datasources/azuremonitor/" }, { - "name": "Apache License", - "url": "https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE" + "name": "License", + "url": "https://github.com/grafana/grafana/blob/HEAD/LICENSE" }, { "name": "Raise issue", @@ -458,6 +458,10 @@ { "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" + }, + { + "name": "Documentation", + "url": "https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/" } ], "logos": { @@ -864,6 +868,10 @@ { "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" + }, + { + "name": "Documentation", + "url": "https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/" } ], "logos": { @@ -1397,6 +1405,10 @@ { "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" + }, + { + "name": "Documentation", + "url": "https://grafana.com/docs/grafana/latest/datasources/mssql/" } ], "logos": { @@ -2105,7 +2117,7 @@ }, { "name": "Raise issue", - "url": "https://github.com/grafana/tempo/issues/new" + "url": "https://github.com/grafana/grafana/issues/new" }, { "name": "Documentation", @@ -2256,6 +2268,10 @@ { "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" + }, + { + "name": "Documentation", + "url": "https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/" } ], "logos": { diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 2c2c0b7236f..5059bb895fc 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -287,6 +287,11 @@ func (c *User) NewRestConfig() *rest.Config { } } +// Implements: apiserver.RestConfigProvider +func (c *User) GetRestConfig(context.Context) (*rest.Config, error) { + return c.NewRestConfig(), nil +} + func (c *User) ResourceClient(t *testing.T, gvr schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { client, err := dynamic.NewForConfig(c.NewRestConfig()) require.NoError(t, err) diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index d74ca23d0be..4d08940becc 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -36,6 +36,232 @@ } } }, + "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/historicjobs": { + "get": { + "tags": [ + "HistoricJob" + ], + "description": "list or watch objects of kind HistoricJob", + "operationId": "listHistoricJob", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "HistoricJob" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/historicjobs/{name}": { + "get": { + "tags": [ + "HistoricJob" + ], + "description": "read the specified HistoricJob", + "operationId": "getHistoricJob", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJob" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJob" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJob" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "HistoricJob" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the HistoricJob", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/jobs": { "get": { "tags": [ @@ -1121,7 +1347,7 @@ "type": "boolean", "default": false }, - "prefix": { + "path": { "description": "Prefix in target file system", "type": "string" } @@ -1130,7 +1356,7 @@ "example": { "folder": "grafan-folder-ref", "branch": "target-branch", - "prefix": "prefix/in/repo/tree", + "path": "path/in/tree", "identifier": false } } @@ -1780,15 +2006,10 @@ "description": "Include the identifier in the exported metadata", "type": "boolean", "default": false - }, - "prefix": { - "description": "Target file prefix", - "type": "string" } } }, "example": { - "prefix": "prefix/in/repo/tree", "history": true, "identifier": false } @@ -2567,7 +2788,7 @@ "type": "boolean", "default": false }, - "prefix": { + "path": { "description": "Prefix in target file system", "type": "string" } @@ -2685,6 +2906,100 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJob": { + "description": "HistoricJob is a history entry of Job. It is used to store Jobs that have been processed.\n\nThe repository name and type are stored as labels.", + "type": "object", + "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", + "type": "string" + }, + "kind": { + "description": "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", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.JobSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.JobStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "provisioning.grafana.app", + "kind": "HistoricJob", + "version": "__internal" + }, + { + "group": "provisioning.grafana.app", + "kind": "HistoricJob", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJobList": { + "type": "object", + "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", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoricJob" + } + ] + } + }, + "kind": { + "description": "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", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "provisioning.grafana.app", + "kind": "HistoricJobList", + "version": "__internal" + }, + { + "group": "provisioning.grafana.app", + "kind": "HistoricJobList", + "version": "v0alpha1" + } + ] + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HistoryItem": { "type": "object", "required": [ @@ -3035,10 +3350,6 @@ "description": "Include the identifier in the exported metadata", "type": "boolean", "default": false - }, - "prefix": { - "description": "Target file prefix", - "type": "string" } } }, diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 80e7d191bbd..c5e01ca0235 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -65,6 +65,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "folder.grafana.app", Version: "v0alpha1", + }, { + Group: "provisioning.grafana.app", + Version: "v0alpha1", }, { Group: "iam.grafana.app", Version: "v0alpha1", diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index a7b61abec50..a30aeba2fc2 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -10,11 +10,13 @@ import ( "testing" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -156,7 +158,7 @@ func TestIntegrationPlaylist(t *testing.T) { }) t.Run("with dual write (file, mode 5)", func(t *testing.T) { - doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + helper := doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "file", // write the files to disk @@ -169,6 +171,33 @@ func TestIntegrationPlaylist(t *testing.T) { featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written }, })) + + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Editor, + GVR: gvr, + }) + + // Folder support needs to be enabled explicitly for this resource + t.Run("ensure writing folders is an error", func(t *testing.T) { + // Create works without folder + obj := helper.LoadYAMLOrJSONFile("testdata/playlist-generate.yaml") + out, err := client.Resource.Create(context.Background(), obj, metav1.CreateOptions{}) + require.NoError(t, err) + + meta, err := utils.MetaAccessor(out) + require.NoError(t, err) + require.Equal(t, int64(1), meta.GetGeneration()) + require.Equal(t, helper.Org1.Editor.Identity.GetUID(), meta.GetCreatedBy()) + require.Equal(t, "", meta.GetUpdatedBy()) + + meta, err = utils.MetaAccessor(obj) + require.NoError(t, err) + meta.SetFolder("FolderUID") + + _, err = client.Resource.Create(context.Background(), obj, metav1.CreateOptions{}) + require.Error(t, err) + require.True(t, apierrors.IsBadRequest(err)) + }) }) t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) { diff --git a/pkg/tests/apis/provisioning/client_test.go b/pkg/tests/apis/provisioning/client_test.go new file mode 100644 index 00000000000..a23cfe04f19 --- /dev/null +++ b/pkg/tests/apis/provisioning/client_test.go @@ -0,0 +1,53 @@ +package provisioning + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +func TestIntegrationProvisioning_Client(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + helper := runGrafana(t) + + ctx := context.Background() + clientFactory := resources.NewClientFactory(&helper.Org1.Admin) + clients, err := clientFactory.Clients(ctx, "default") + require.NoError(t, err) + + t.Run("dashboard client support", func(t *testing.T) { + dash, err := clients.Dashboard() + require.NoError(t, err) + require.NotNil(t, dash) + + client, _, err := clients.ForResource(schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Version: "v0alpha1", + }) + require.NoError(t, err) + require.Equal(t, dash, client, "expecting the default dashboard to be version0") + + // With empty version, we should get the preferred version (v1alpha1) + client, _, err = clients.ForResource(schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + }) + require.NoError(t, err) + require.Equal(t, dash, client, "expecting the default dashboard to be version0") + + client, _, err = clients.ForKind(schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Kind: "Dashboard", + }) + require.NoError(t, err) + require.Equal(t, dash, client, "same client when requested by kind") + }) +} diff --git a/pkg/tests/apis/provisioning/exportunifiedtorepository/repository.json.tmpl b/pkg/tests/apis/provisioning/exportunifiedtorepository/repository.json.tmpl new file mode 100644 index 00000000000..a7abad44e02 --- /dev/null +++ b/pkg/tests/apis/provisioning/exportunifiedtorepository/repository.json.tmpl @@ -0,0 +1,21 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ .Name }}" + }, + "spec": { + "description": "Test", + "local": { + "path": "{{ .h.ProvisioningPath }}" + }, + "sync": { + "enabled": true, + "target": "instance", + "intervalSeconds": 60 + }, + "title": "ExportToUnifiedRepository test repository", + "type": "local", + "workflows": ["write"] + } +} diff --git a/pkg/tests/apis/provisioning/exportunifiedtorepository/root_dashboard.json b/pkg/tests/apis/provisioning/exportunifiedtorepository/root_dashboard.json new file mode 100644 index 00000000000..a144c917c64 --- /dev/null +++ b/pkg/tests/apis/provisioning/exportunifiedtorepository/root_dashboard.json @@ -0,0 +1,162 @@ +{ + "apiVersion": "dashboard.grafana.app/v1alpha1", + "kind": "Dashboard", + "metadata": { + "name": "root_dashboard", + "namespace": "default" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "description": "This is on the root level of the repository.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu", + "seriesBy": "last" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Y axis on the right side", + "axisPlacement": "right", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 7, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "decimals": 3, + "fieldMinMax": false, + "links": [ + { + "oneClick": true, + "targetBlank": true, + "title": "Test test", + "url": "https://github.com/grafana/grafana" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "kvoltamp" + }, + "overrides": [] + }, + "gridPos": { + "h": 24, + "w": 22, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Test panel", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [ + "grafana", + "dashboard", + "root", + "test" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "Pacific/Kiritimati", + "title": "Test dashboard on root level", + "uid": "", + "version": 0, + "weekStart": "saturday" + } +} diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go new file mode 100644 index 00000000000..a319f069e26 --- /dev/null +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -0,0 +1,353 @@ +package provisioning + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" + "os" + "path" + "strings" + "testing" + "text/template" + "time" + + gh "github.com/google/go-github/v69/github" + ghmock "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +type provisioningTestHelper struct { + *apis.K8sTestHelper + ProvisioningPath string + + Repositories *apis.K8sResourceClient + Jobs *apis.K8sResourceClient + HistoricJobs *apis.K8sResourceClient + Folders *apis.K8sResourceClient + Dashboards *apis.K8sResourceClient + AdminREST *rest.RESTClient + ViewerREST *rest.RESTClient +} + +func (h *provisioningTestHelper) AwaitJobSuccess(t *testing.T, ctx context.Context, jobName string) { + t.Helper() + if !assert.EventuallyWithT(t, func(collect *assert.CollectT) { + jobs, err := h.HistoricJobs.Resource.List(ctx, metav1.ListOptions{ + LabelSelector: jobs.LabelJobOriginalName + "=" + jobName, + }) + if assert.NoError(collect, err) && assert.NotEmpty(collect, jobs.Items, "no historic jobs found yet") { + for _, job := range jobs.Items { + state := mustNestedString(job.Object, "status", "state") + if state == "" { + // The job hasn't gotten its state yet. We do two requests: one to insert the job, one to set the status. + assert.Fail(collect, "job '%s' has no state yet", jobName) + } + + // We can fail fast once the job is here: HistoricJobs are immutable. + require.Equal(t, string(provisioning.JobStateSuccess), state, "historic job '%s' was not successful", jobName) + } + } + }, time.Second*5, time.Millisecond*20) { + // We also want to add the job details to the error when it fails. + job, err := h.Jobs.Resource.Get(ctx, jobName, metav1.GetOptions{}) + if err != nil { + t.Logf("failed to get job details for further help: %v", err) + } else { + t.Logf("job details: %+v", job.Object) + } + t.FailNow() + } +} + +func (h *provisioningTestHelper) AwaitJobs(t *testing.T, repoName string) { + t.Helper() + + // First, we wait for all jobs for the repository to disappear (i.e. complete/fail). + require.EventuallyWithT(t, func(collect *assert.CollectT) { + list, err := h.Jobs.Resource.List(context.Background(), metav1.ListOptions{}) + if assert.NoError(collect, err, "failed to list active jobs") { + for _, elem := range list.Items { + // TODO: Use the spec field of the job. + if elem.GetLabels()["repository"] == repoName { + collect.Errorf("there are still remaining jobs for %s: %+v", repoName, elem) + return + } + } + } + }, time.Second*5, time.Millisecond*20) + + // Then, as all jobs are now historic jobs, we make sure they are successful. + list, err := h.HistoricJobs.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err, "failed to list historic jobs") + + for _, elem := range list.Items { + // TODO: Use the spec field of the job. + if elem.GetLabels()["repository"] == repoName { + require.Equal(t, string(provisioning.JobStateSuccess), mustNestedString(elem.Object, "status", "state"), "job %s failed: %+v", elem.GetName(), elem.Object) + } + } +} + +// RenderObject reads the filePath and renders it as a template with the given values. +// The template is expected to be a YAML or JSON file. +// +// The values object is mutated to also include the helper property as `h`. +func (h *provisioningTestHelper) RenderObject(t *testing.T, filePath string, values map[string]any) *unstructured.Unstructured { + t.Helper() + file := h.LoadFile(filePath) + + if values == nil { + values = make(map[string]any) + } + values["h"] = h + + tmpl, err := template.New(filePath).Parse(string(file)) + require.NoError(t, err, "failed to parse template") + + var buf strings.Builder + err = tmpl.Execute(&buf, values) + require.NoError(t, err, "failed to execute template") + + return h.LoadYAMLOrJSON(buf.String()) +} + +// CopyToProvisioningPath copies a file to the provisioning path. +// The from path is relative to test file's directory. +func (h *provisioningTestHelper) CopyToProvisioningPath(t *testing.T, from, to string) { + file := h.LoadFile(from) + err := os.WriteFile(path.Join(h.ProvisioningPath, to), file, 0600) + require.NoError(t, err, "failed to write file to provisioning path") +} + +type grafanaOption func(opts *testinfra.GrafanaOpts) + +// Useful for debugging a test in development. +// +//lint:ignore U1000 This is used when needed while debugging. +//nolint:golint,unused +func withLogs(opts *testinfra.GrafanaOpts) { + opts.EnableLog = true +} + +func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper { + apiserver.ClearRestConfig() + + provisioningPath := t.TempDir() + opts := testinfra.GrafanaOpts{ + AppModeProduction: false, // required for experimental APIs + EnableFeatureToggles: []string{ + featuremgmt.FlagProvisioning, + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagNestedFolders, + featuremgmt.FlagUnifiedStorageSearch, + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: grafanarest.Mode5, + }, + "folders.folder.grafana.app": { + DualWriterMode: grafanarest.Mode5, + }, + }, + PermittedProvisioningPaths: ".|" + provisioningPath, + } + for _, o := range options { + o(&opts) + } + helper := apis.NewK8sTestHelper(t, opts) + + helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( + ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{})), + ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), + ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{})), + ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{})), + ghmock.WithRequestMatchHandler(ghmock.GetReposBranchesByOwnerByRepoByBranch, ghAlwaysWrite(t, &gh.Branch{})), + ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, ghAlwaysWrite(t, &gh.Tree{})), + ghmock.WithRequestMatchHandler( + ghmock.DeleteReposHooksByOwnerByRepoByHookId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ), + ) + + repositories := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: provisioning.RepositoryResourceInfo.GroupVersionResource(), + }) + jobs := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: provisioning.JobResourceInfo.GroupVersionResource(), + }) + historicJobs := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: provisioning.HistoricJobResourceInfo.GroupVersionResource(), + }) + folders := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: folder.FolderResourceInfo.GroupVersionResource(), + }) + dashboards := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: dashboard.DashboardResourceInfo.GroupVersionResource(), + }) + + // Repo client, but less guard rails. Useful for subresources. We'll need this later... + restClient := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{ + Group: "provisioning.grafana.app", Version: "v0alpha1", + }) + + viewerClient := helper.Org1.Viewer.RESTClient(t, &schema.GroupVersion{ + Group: "provisioning.grafana.app", Version: "v0alpha1", + }) + + deleteAll := func(client *apis.K8sResourceClient) error { + ctx := context.Background() + list, err := client.Resource.List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } + for _, resource := range list.Items { + if err := client.Resource.Delete(ctx, resource.GetName(), metav1.DeleteOptions{}); err != nil { + return err + } + } + return nil + } + + require.NoError(t, deleteAll(dashboards), "deleting all dashboards") + require.NoError(t, deleteAll(folders), "deleting all folders") + require.NoError(t, deleteAll(repositories), "deleting all repositories") + + return &provisioningTestHelper{ + ProvisioningPath: provisioningPath, + K8sTestHelper: helper, + + Repositories: repositories, + AdminREST: restClient, + ViewerREST: viewerClient, + Jobs: jobs, + HistoricJobs: historicJobs, + Folders: folders, + Dashboards: dashboards, + } +} + +func ghAlwaysWrite(t *testing.T, body any) http.HandlerFunc { + marshalled := ghmock.MustMarshal(body) + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write(marshalled) + require.NoError(t, err, "failed to write body in mock") + }) +} + +func ghHandleTree(t *testing.T, refs map[string][]*gh.TreeEntry) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sha := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + require.NotEmpty(t, sha, "sha path parameter was missing?") + + entries := refs[sha] + require.NotNil(t, entries, "no entries for sha %s", sha) + + tree := &gh.Tree{ + SHA: gh.Ptr(sha), + Truncated: gh.Ptr(false), + Entries: entries, + } + + _, err := w.Write(ghmock.MustMarshal(tree)) + require.NoError(t, err, "failed to write body in mock") + }) +} + +func mustNestedString(obj map[string]interface{}, fields ...string) string { + v, _, err := unstructured.NestedString(obj, fields...) + if err != nil { + panic(err) + } + return v +} + +func asJSON(obj any) []byte { + jj, _ := json.Marshal(obj) + return jj +} + +func treeEntryDir(dirName string, sha string) *gh.TreeEntry { + return &gh.TreeEntry{ + SHA: gh.Ptr(sha), + Path: gh.Ptr(dirName), + Type: gh.Ptr("tree"), + Mode: gh.Ptr("040000"), + } +} + +func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *provisioning.Repository { + bytes, err := obj.MarshalJSON() + require.NoError(t, err) + + repo := &provisioning.Repository{} + err = json.Unmarshal(bytes, repo) + require.NoError(t, err) + + return repo +} + +func treeEntry(fpath string, content []byte) *gh.TreeEntry { + sha := sha256.Sum256(content) + + return &gh.TreeEntry{ + SHA: gh.Ptr(hex.EncodeToString(sha[:])), + Path: gh.Ptr(fpath), + Size: gh.Ptr(len(content)), + Type: gh.Ptr("blob"), + Mode: gh.Ptr("100644"), + Content: gh.Ptr(string(content)), + } +} + +func repoContent(fpath string, content []byte) *gh.RepositoryContent { + sha := sha256.Sum256(content) + typ := "blob" + if strings.HasSuffix(fpath, "/") { + typ = "tree" + } + + return &gh.RepositoryContent{ + SHA: gh.Ptr(hex.EncodeToString(sha[:])), + Name: gh.Ptr(path.Base(fpath)), + Path: &fpath, + Size: gh.Ptr(len(content)), + Type: &typ, + Content: gh.Ptr(string(content)), + } +} diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 2199cea7041..358057dc421 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -2,124 +2,366 @@ package provisioning import ( "context" - "encoding/json" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" "testing" - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tests/testsuite" + gh "github.com/google/go-github/v69/github" + ghmock "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/tests/apis" ) -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationProvisioning(t *testing.T) { +func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} ctx := context.Background() - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs - EnableFeatureToggles: []string{ - featuremgmt.FlagProvisioning, - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: rest.Mode5, - }, - "folders.folder.grafana.app": { - DualWriterMode: rest.Mode5, - }, - }, - }) - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: provisioning.RepositoryResourceInfo.GroupVersionResource(), - }) - - folderClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: folder.FolderResourceInfo.GroupVersionResource(), - }) - - dashboardClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: dashboard.DashboardResourceInfo.GroupVersionResource(), - }) - - // Repo client, but less guard rails. Useful for subresources. We'll need this later... - restClient := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{ - Group: "provisioning.grafana.app", Version: "v0alpha1", - }) - _ = restClient - - cleanSlate := func(t *testing.T) { - deleteAll := func(client *apis.K8sResourceClient) error { - list, err := client.Resource.List(ctx, metav1.ListOptions{}) - if err != nil { - return err - } - for _, resource := range list.Items { - if err := client.Resource.Delete(ctx, resource.GetName(), metav1.DeleteOptions{}); err != nil { - return err - } - } - return nil - } - - require.NoError(t, deleteAll(dashboardClient), "deleting all dashboards") - require.NoError(t, deleteAll(folderClient), "deleting all folders") - require.NoError(t, deleteAll(client), "deleting all repositories") + inputFiles := []string{ + "testdata/github-readonly.json.tmpl", + "testdata/local-readonly.json.tmpl", } - cleanSlate(t) - t.Run("Creating and getting repositories", func(t *testing.T) { - cleanSlate(t) + for _, inputFilePath := range inputFiles { + t.Run(inputFilePath, func(t *testing.T) { + input := helper.RenderObject(t, inputFilePath, nil) - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") - for _, inputFilePath := range []string{ - "testdata/local-devenv.json", - "testdata/github-example.json", - } { - t.Run(inputFilePath, func(t *testing.T) { - input := helper.LoadYAMLOrJSONFile(inputFilePath) - expectedOutput, err := json.MarshalIndent(input.Object["spec"], "", " ") - require.NoError(t, err, "failed to marshal JSON from input spec") + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") - _, err = client.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") + // Move encrypted token mutation + token, found, err := unstructured.NestedString(output.Object, "spec", "github", "encryptedToken") + require.NoError(t, err, "encryptedToken is not a string") + if found { + unstructured.RemoveNestedField(input.Object, "spec", "github", "token") + err = unstructured.SetNestedField(input.Object, token, "spec", "github", "encryptedToken") + require.NoError(t, err, "unable to copy encrypted token") + } - output, err := client.Resource.Get(ctx, mustNestedString(input.Object, "metadata", "name"), metav1.GetOptions{}) - require.NoError(t, err, "failed to read back resource") - outputJSON, err := json.MarshalIndent(output.Object["spec"], "", " ") - require.NoError(t, err, "failed to marshal JSON from read back resource") + // Marshal as real objects to ",omitempty" values are tested properly + expectedRepo := unstructuredToRepository(t, input) + returnedRepo := unstructuredToRepository(t, output) + require.Equal(t, expectedRepo.Spec, returnedRepo.Spec) - require.JSONEq(t, string(expectedOutput), string(outputJSON)) - }) + // A viewer should not be able to see the same thing + var statusCode int + rsp := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(name). + Do(context.Background()) + require.Error(t, rsp.Error()) + rsp.StatusCode(&statusCode) + require.Equal(t, http.StatusForbidden, statusCode) + + // Viewer can see file listing + rsp = helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(name). + Suffix("files/"). + Do(context.Background()) + require.NoError(t, rsp.Error()) + }) + } + + // Viewer can see settings listing + t.Run("viewer has access to list", func(t *testing.T) { + settings := &provisioning.RepositoryViewList{} + rsp := helper.ViewerREST.Get(). + Namespace("default"). + Suffix("settings"). + Do(context.Background()) + require.NoError(t, rsp.Error()) + err := rsp.Into(settings) + require.NoError(t, err) + require.Len(t, settings.Items, len(inputFiles)) + }) + + t.Run("Repositories are reported in stats", func(t *testing.T) { + report := apis.DoRequest(helper.K8sTestHelper, apis.RequestParams{ + Method: http.MethodGet, + Path: "/api/admin/usage-report-preview", + User: helper.K8sTestHelper.Org1.Admin, + }, &usagestats.Report{}) + + stats := map[string]any{} + for k, v := range report.Result.Metrics { + if strings.HasPrefix(k, "stats.repository.") { + stats[k] = v + } } + require.Equal(t, map[string]any{ + "stats.repository.github.count": 1.0, + "stats.repository.local.count": 1.0, + }, stats) }) } -func mustNestedString(obj map[string]interface{}, fields ...string) string { - v, _, _ := unstructured.NestedString(obj, fields...) - return v +func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( + ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{Name: gh.Ptr("github-user")})), + ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), + ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{ID: gh.Ptr(int64(123))})), + ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{ID: gh.Ptr(int64(234))})), + ghmock.WithRequestMatchHandler( + ghmock.GetReposBranchesByOwnerByRepoByBranch, + ghAlwaysWrite(t, &gh.Branch{ + Name: gh.Ptr("main"), + Commit: &gh.RepositoryCommit{SHA: gh.Ptr("deadbeef")}, + }), + ), + ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, + ghHandleTree(t, map[string][]*gh.TreeEntry{ + "deadbeef": { + treeEntryDir("grafana", "subtree"), + }, + "subtree": { + treeEntry("dashboard.json", helper.LoadFile("testdata/all-panels.json")), + treeEntryDir("subdir", "subtree2"), + treeEntry("subdir/dashboard2.yaml", helper.LoadFile("testdata/text-options.json")), + }, + })), + ghmock.WithRequestMatchHandler( + ghmock.GetReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pathRegex := regexp.MustCompile(`/repos/[^/]+/[^/]+/contents/(.*)`) + matches := pathRegex.FindStringSubmatch(r.URL.Path) + require.NotNil(t, matches, "no match for contents?") + path := matches[1] + + var err error + switch path { + case "grafana/dashboard.json": + _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/all-panels.json")))) + case "grafana/subdir/dashboard2.yaml": + _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/text-options.json")))) + default: + t.Fatalf("got unexpected path: %s", path) + } + require.NoError(t, err) + }), + ), + ) + + const repo = "github-create-test" + _, err := helper.Repositories.Resource.Update(ctx, + helper.RenderObject(t, "testdata/github-readonly.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + "SyncTarget": "instance", + "Path": "grafana/", + }), + metav1.UpdateOptions{}, + ) + require.NoError(t, err) + + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("sync"). + Body(asJSON(provisioning.SyncJobOptions{ + Incremental: false, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + obj, err := result.Get() + require.NoError(t, err, "expecting to be able to sync repository") + obj2, ok := obj.(*unstructured.Unstructured) + if !ok { + require.Fail(t, "expected unstructured response, %T", obj) + } + job := obj2.GetName() + require.NotEmpty(t, job) + + helper.AwaitJobSuccess(t, ctx, job) + + // By now, we should have synced, meaning we have data to read in the local Grafana instance! + + found, err := helper.Dashboards.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "can list values") + + names := []string{} + for _, v := range found.Items { + names = append(names, v.GetName()) + } + assert.Contains(t, names, "n1jR8vnnz", "should contain dashboard.json's contents") + assert.Contains(t, names, "WZ7AhQiVz", "should contain dashboard2.yaml's contents") +} + +func TestIntegrationProvisioning_SafePathUsages(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "local-safe-path-usages" + // Set up the repository. + localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{"Name": repo}) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // Write a file + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "all-panels.json"). + Body(helper.LoadFile("testdata/all-panels.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "expecting to be able to create file") + + // Write a file with a bad path + result = helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "test", "..", "..", "all-panels.json"). + Body(helper.LoadFile("testdata/all-panels.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.Error(t, result.Error(), "invalid path should return error") + + // Read a file + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "all-panels.json") + require.NoError(t, err, "valid path should be fine") + + // Read a file with a bad path + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "../../all-panels.json") + require.Error(t, err, "invalid path should not be fine") +} + +func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "local-tmp" + // Set up the repository and the file to import. + helper.CopyToProvisioningPath(t, "testdata/all-panels.json", "all-panels.json") + + localTmp := helper.RenderObject(t, "testdata/local-readonly.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + }) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // Make sure the repo can see the file + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "all-panels.json") + require.NoError(t, err, "valid path should be fine") + + // But the dashboard shouldn't exist yet + const allPanels = "n1jR8vnnz" + _, err = helper.Dashboards.Resource.Get(ctx, allPanels, metav1.GetOptions{}) + require.Error(t, err, "no all-panels dashboard should exist") + + // Now, we import it, such that it may exist + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("sync"). + SetHeader("Content-Type", "application/json"). + Body(asJSON(provisioning.SyncJobOptions{ + Incremental: false, + })). + Do(ctx) + + obj, err := result.Get() + require.NoError(t, err, "expecting to be able to sync repository") + + obj2, ok := obj.(*unstructured.Unstructured) + if !ok { + require.Fail(t, "expected unstructured response, %T", obj) + } + job := obj2.GetName() + require.NotEmpty(t, job) + + // Wait for the async job to finish + helper.AwaitJobSuccess(t, ctx, job) + + found, err := helper.Dashboards.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "can list values") + + names := []string{} + for _, v := range found.Items { + names = append(names, v.GetName()) + } + require.Contains(t, names, allPanels, "all-panels dashboard should now exist") +} + +func TestProvisioning_ExportUnifiedToRepository(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + // Set up dashboards first, then the repository, and finally export. + dashboard := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/root_dashboard.json") + _, err := helper.Dashboards.Resource.Create(ctx, dashboard, metav1.CreateOptions{}) + require.NoError(t, err, "should be able to create prerequisite dashboard") + + // Now for the repository. + const repo = "local-repository" + createBody := helper.RenderObject(t, "exportunifiedtorepository/repository.json.tmpl", map[string]any{"Name": repo}) + _, err = helper.Repositories.Resource.Create(ctx, createBody, metav1.CreateOptions{}) + require.NoError(t, err, "should be able to create repository") + + // Now export... + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("export"). + SetHeader("Content-Type", "application/json"). + Body(asJSON(&provisioning.ExportJobOptions{ + Folder: "", // export entire instance + Path: "", // no prefix necessary for testing + Identifier: true, // doesn't _really_ matter, but handy for debugging. + })). + Do(ctx) + require.NoError(t, result.Error()) + + // And time to assert. + helper.AwaitJobs(t, repo) + + fpath := filepath.Join(helper.ProvisioningPath, slugify.Slugify(mustNestedString(dashboard.Object, "spec", "title"))+".json") + _, err = os.Stat(fpath) + require.NoError(t, err, "exported file was not created at path %s", fpath) } diff --git a/pkg/tests/apis/provisioning/testdata/all-panels.json b/pkg/tests/apis/provisioning/testdata/all-panels.json new file mode 120000 index 00000000000..ed9366cbb10 --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/all-panels.json @@ -0,0 +1 @@ +../../../../../devenv/dev-dashboards/all-panels.json \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/github-example.json b/pkg/tests/apis/provisioning/testdata/github-example.json deleted file mode 100644 index b9bcba83779..00000000000 --- a/pkg/tests/apis/provisioning/testdata/github-example.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "apiVersion": "provisioning.grafana.app/v0alpha1", - "kind": "Repository", - "metadata": { - "name": "github-example" - }, - "spec": { - "title": "Github Example", - "description": "load resources from github", - "type": "github", - "github": { - "url": "https://github.com/grafana/git-ui-sync-demo", - "branch": "dummy-branch", - "generateDashboardPreviews": true, - "token": "github_pat_dummy" - }, - "sync": { - "enabled": false, - "target": "", - "intervalSeconds": 60 - }, - "workflows": ["push"] - } -} \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl new file mode 100644 index 00000000000..220b0936b0e --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl @@ -0,0 +1,25 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ or .Name "github-repository" }}" + }, + "spec": { + "title": "{{ or .Title .Name "GitHub repository" }}", + "description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", + "type": "github", + "github": { + "url": "{{ or .URL "https://github.com/grafana/git-ui-sync-demo" }}", + "branch": "{{ or .Branch "dummy" }}", + "generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }}, + "token": "{{ or .Token "github_pat_dummy" }}", + "path": "{{ or .Path "grafana/" }}" + }, + "sync": { + "enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, + "target": "{{ or .SyncTarget "folder" }}", + "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} + }, + "workflows": [] + } +} diff --git a/pkg/tests/apis/provisioning/testdata/local-devenv.json b/pkg/tests/apis/provisioning/testdata/local-devenv.json deleted file mode 100644 index 218f2715311..00000000000 --- a/pkg/tests/apis/provisioning/testdata/local-devenv.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "apiVersion": "provisioning.grafana.app/v0alpha1", - "kind": "Repository", - "metadata": { - "name": "local-devenv" - }, - "spec": { - "title": "Load devenv dashboards", - "description": "Load /devenv/dev-dashboards (from root of repository)", - "workflows": ["write"], - "sync": { - "enabled": true, - "target": "mirror", - "intervalSeconds": 60 - }, - "type": "local", - "local": { - "path": "devenv/dev-dashboards" - } - } -} \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/local-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/local-readonly.json.tmpl new file mode 100644 index 00000000000..0c3c418c20f --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/local-readonly.json.tmpl @@ -0,0 +1,21 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ or .Name "local-repository" }}" + }, + "spec": { + "title": "{{ or .Title .Name "Local repository" }}", + "description": "{{ or .Description .Name "Load grafana dashboard from local repository" }}", + "workflows": [], + "sync": { + "enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, + "target": "{{ or .SyncTarget "folder" }}", + "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} + }, + "type": "local", + "local": { + "path": "{{ or .Path .h.ProvisioningPath }}" + } + } +} \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/local-write.json.tmpl b/pkg/tests/apis/provisioning/testdata/local-write.json.tmpl new file mode 100644 index 00000000000..1745e9f9769 --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/local-write.json.tmpl @@ -0,0 +1,21 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ or .Name "local-repository" }}" + }, + "spec": { + "title": "{{ or .Title .Name "Local repository" }}", + "description": "{{ or .Description .Name "Load grafana dashboard from local repository" }}", + "workflows": ["write"], + "sync": { + "enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, + "target": "{{ or .SyncTarget "folder" }}", + "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} + }, + "type": "local", + "local": { + "path": "{{ or .Path .h.ProvisioningPath }}" + } + } +} \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/text-options.json b/pkg/tests/apis/provisioning/testdata/text-options.json new file mode 120000 index 00000000000..68a1964ed1d --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/text-options.json @@ -0,0 +1 @@ +../../../../../devenv/dev-dashboards/panel-text/text-options.json \ No newline at end of file diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index d8c412f9618..f0afa3ddc05 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -80,7 +80,7 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes runstore = true } - env, err := server.InitializeForTest(t, cfg, serverOpts, apiServerOpts) + env, err := server.InitializeForTest(t, t, cfg, serverOpts, apiServerOpts) require.NoError(t, err) require.NotNil(t, env.Cfg) @@ -490,14 +490,26 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) } } + if opts.PermittedProvisioningPaths != "" { + _, err = pathsSect.NewKey("permitted_provisioning_paths", opts.PermittedProvisioningPaths) + require.NoError(t, err) + } dbSection, err := getOrCreateSection("database") require.NoError(t, err) _, err = dbSection.NewKey("query_retries", fmt.Sprintf("%d", queryRetries)) require.NoError(t, err) - _, err = dbSection.NewKey("max_open_conn", "2") + if db.IsTestDBSpanner() { + _, err = dbSection.NewKey("max_open_conn", "20") + } else { + _, err = dbSection.NewKey("max_open_conn", "2") + } require.NoError(t, err) - _, err = dbSection.NewKey("max_idle_conn", "2") + if db.IsTestDBSpanner() { + _, err = dbSection.NewKey("max_idle_conn", "20") + } else { + _, err = dbSection.NewKey("max_idle_conn", "2") + } require.NoError(t, err) cfgPath := filepath.Join(cfgDir, "test.ini") @@ -542,6 +554,7 @@ type GrafanaOpts struct { QueryRetries int64 GrafanaComAPIURL string UnifiedStorageConfig map[string]setting.UnifiedStorageConfig + PermittedProvisioningPaths string GrafanaComSSOAPIToken string LicensePath string EnableRecordingRules bool diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 874c4161741..457ed0c9baa 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -48,7 +48,7 @@ type TempoQuery struct { // Defines the maximum number of spans per spanset that are returned from Tempo Spss *int64 `json:"spss,omitempty"` Filters []TraceqlFilter `json:"filters"` - // Filters that are used to query the metrics summary + // deprecated Filters that are used to query the metrics summary GroupBy []TraceqlFilter `json:"groupBy,omitempty"` // The type of the table that is used to display the search results TableType *SearchTableType `json:"tableType,omitempty"` diff --git a/pkg/util/xorm/dialect_spanner.go b/pkg/util/xorm/dialect_spanner.go index 4269107cbb6..2460f4228dd 100644 --- a/pkg/util/xorm/dialect_spanner.go +++ b/pkg/util/xorm/dialect_spanner.go @@ -9,6 +9,10 @@ import ( "strings" _ "github.com/googleapis/go-sql-spanner" + spannerdriver "github.com/googleapis/go-sql-spanner" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "xorm.io/core" ) @@ -370,3 +374,52 @@ func (s *spanner) GetIndexes(tableName string) (map[string]*core.Index, error) { } return indexes, res.Err() } + +func (s *spanner) CreateSequenceGenerator(db *sql.DB) (SequenceGenerator, error) { + dsn := s.DataSourceName() + connectorConfig, err := spannerdriver.ExtractConnectorConfig(dsn) + if err != nil { + return nil, err + } + + if UsePlainText(connectorConfig) { + // Plain-text means we're either using spannertest or Spanner emulator. + // Switch to fake in-memory sequence number generator in that case. + // + // Using database-based sequence generator doesn't work with emulator, as emulator + // only supports single transaction. If there is already another transaction started + // generating new ID via database-based sequence generator would always fail. + return newInMemSequenceGenerator(), nil + } + + return newSequenceGenerator(db), nil +} + +func UsePlainText(connectorConfig spannerdriver.ConnectorConfig) bool { + if strval, ok := connectorConfig.Params["useplaintext"]; ok { + if val, err := strconv.ParseBool(strval); err == nil { + return val + } + } + return false +} + +// SpannerConnectorConfigToClientOptions is adapted from https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L341-L477, from version 1.11.1. +func SpannerConnectorConfigToClientOptions(connectorConfig spannerdriver.ConnectorConfig) []option.ClientOption { + var opts []option.ClientOption + if connectorConfig.Host != "" { + opts = append(opts, option.WithEndpoint(connectorConfig.Host)) + } + if strval, ok := connectorConfig.Params["credentials"]; ok { + opts = append(opts, option.WithCredentialsFile(strval)) + } + if strval, ok := connectorConfig.Params["credentialsjson"]; ok { + opts = append(opts, option.WithCredentialsJSON([]byte(strval))) + } + if UsePlainText(connectorConfig) { + opts = append(opts, + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication()) + } + return opts +} diff --git a/pkg/util/xorm/engine.go b/pkg/util/xorm/engine.go index 8db50041ae2..4d29facbd77 100644 --- a/pkg/util/xorm/engine.go +++ b/pkg/util/xorm/engine.go @@ -44,7 +44,7 @@ type Engine struct { tagHandlers map[string]tagHandler defaultContext context.Context - sequenceGenerator *sequenceGenerator // If not nil, this generator is used to generate auto-increment values for inserts. + sequenceGenerator SequenceGenerator // If not nil, this generator is used to generate auto-increment values for inserts. } // CondDeleted returns the conditions whether a record is soft deleted. @@ -239,9 +239,6 @@ func (engine *Engine) NewSession() *Session { // Close the engine func (engine *Engine) Close() error { - if engine.sequenceGenerator != nil { - engine.sequenceGenerator.close() - } return engine.db.Close() } diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index e9406624f26..9a1231c42ee 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -7,6 +7,8 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.10.0 + google.golang.org/api v0.220.0 + google.golang.org/grpc v1.71.0 xorm.io/builder v0.3.6 xorm.io/core v0.7.3 ) @@ -23,9 +25,9 @@ require ( github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -40,7 +42,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect @@ -52,14 +54,12 @@ require ( golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 // indirect - google.golang.org/api v0.220.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index 557aad278ad..ce99b6b949f 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -659,8 +659,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= -github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -681,8 +681,8 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= -github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -935,8 +935,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= @@ -1207,8 +1207,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1521,10 +1521,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o= -google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1566,8 +1566,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/util/xorm/sequence.go b/pkg/util/xorm/sequence.go index bb74e2064b6..19192db96f3 100644 --- a/pkg/util/xorm/sequence.go +++ b/pkg/util/xorm/sequence.go @@ -3,65 +3,153 @@ package xorm import ( "context" "database/sql" - "errors" "fmt" + "sync" ) +// batchState represents the state of a sequence batch +type batchState struct { + mu sync.Mutex + nextValue int64 + lastValueInBatch int64 +} + type sequenceGenerator struct { db *sql.DB sequencesTable string + batchSize int64 + + mu sync.Mutex + batchStates map[string]*batchState // Track sequence batches per key (table:column) } func newSequenceGenerator(db *sql.DB) *sequenceGenerator { return &sequenceGenerator{ db: db, sequencesTable: "autoincrement_sequences", + batchSize: 100, // Default batch size + batchStates: make(map[string]*batchState), } } +func (sg *sequenceGenerator) Reset() { + sg.mu.Lock() + defer sg.mu.Unlock() + sg.batchStates = make(map[string]*batchState) +} + func (sg *sequenceGenerator) Next(ctx context.Context, table, column string) (int64, error) { - // Current implementation fetches new value for each Next call. key := fmt.Sprintf("%s:%s", table, column) + // First get or create the state with a global lock (only for map access) + sg.mu.Lock() + state, exists := sg.batchStates[key] + if !exists { + state = &batchState{ + nextValue: 0, + lastValueInBatch: -1, + } + sg.batchStates[key] = state + } + sg.mu.Unlock() // Release global lock as soon as possible + + // Now lock only the specific sequence state + state.mu.Lock() + defer state.mu.Unlock() + + // If we've used all values in the current batch, get a new batch + if state.nextValue > state.lastValueInBatch { + start, end, err := sg.allocateNewBatch(ctx, key) + if err != nil { + return 0, err + } + state.nextValue = start + state.lastValueInBatch = end + } + + // Return the next value from the batch + val := state.nextValue + state.nextValue++ + return val, nil +} + +// allocateNewBatch retrieves a new batch of sequence values from the database. +// It returns the start and end values of the new batch on success. +func (sg *sequenceGenerator) allocateNewBatch(ctx context.Context, key string) (start, end int64, retErr error) { tx, err := sg.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) if err != nil { - return 0, err + return 0, 0, err } - // TODO: "FOR UPDATE". (Somehow this doesn't seem to be supported in Spanner emulator?) - r, err := tx.QueryContext(ctx, "SELECT next_value FROM "+sg.sequencesTable+" WHERE name = ?", key) - if err != nil { - err2 := tx.Rollback() - return 0, errors.Join(err, err2) - } - defer r.Close() - - // Sequence doesn't exist yet. Return 1, and put 2 into the table. - if !r.Next() { - if err := r.Err(); err != nil { - return 0, errors.Join(err, tx.Rollback()) + defer func() { + if retErr != nil { + tx.Rollback() } - val := int64(1) + }() - _, err := tx.ExecContext(ctx, "INSERT INTO "+sg.sequencesTable+" (name, next_value) VALUES(?, ?)", key, val+1) + // Query the current sequence value + rows, err := tx.QueryContext(ctx, "SELECT next_value FROM "+sg.sequencesTable+" WHERE name = ?", key) + if err != nil { + return 0, 0, err + } + defer rows.Close() + + // Handle case where sequence doesn't exist yet + if !rows.Next() { + if err = rows.Err(); err != nil { + return 0, 0, err + } + + // This is a new sequence - start from 1 and allocate a batch + batchEnd := sg.batchSize + nextBatchStart := batchEnd + 1 + + // Insert the next batch start value + _, err = tx.ExecContext(ctx, + "INSERT INTO "+sg.sequencesTable+" (name, next_value) VALUES(?, ?)", + key, nextBatchStart) if err != nil { - return 0, errors.Join(err, tx.Rollback()) + return 0, 0, err } - return val, tx.Commit() + // Commit the transaction + if err = tx.Commit(); err != nil { + return 0, 0, err + } + + return 1, batchEnd, nil } - var val int64 - if err := r.Scan(&val); err != nil { - return 0, errors.Join(err, tx.Rollback()) + // Sequence exists - read current value and allocate next batch + var batchStart int64 + if err = rows.Scan(&batchStart); err != nil { + return 0, 0, err } - _, err = tx.ExecContext(ctx, "UPDATE "+sg.sequencesTable+" SET next_value = ? WHERE name = ?", val+1, key) + batchEnd := batchStart + sg.batchSize - 1 + nextBatchStart := batchEnd + 1 + + // Update the next batch start value + _, err = tx.ExecContext(ctx, + "UPDATE "+sg.sequencesTable+" SET next_value = ? WHERE name = ?", + nextBatchStart, key) if err != nil { - return 0, errors.Join(err, tx.Rollback()) + return 0, 0, err } - return val, tx.Commit() + // Commit the transaction + if err = tx.Commit(); err != nil { + return 0, 0, err + } + + return batchStart, batchEnd, nil +} + +// SetBatchSize allows changing the batch size +func (sg *sequenceGenerator) SetBatchSize(size int64) { + sg.mu.Lock() + defer sg.mu.Unlock() + sg.batchSize = size } func (sg *sequenceGenerator) close() { diff --git a/pkg/util/xorm/sequence_inmem.go b/pkg/util/xorm/sequence_inmem.go new file mode 100644 index 00000000000..1d5cfacea76 --- /dev/null +++ b/pkg/util/xorm/sequence_inmem.go @@ -0,0 +1,46 @@ +package xorm + +import ( + "context" + "fmt" + "math/rand/v2" + "sync" +) + +type inMemSequenceGenerator struct { + sequencesMu sync.Mutex + nextValues map[string]int +} + +func newInMemSequenceGenerator() *inMemSequenceGenerator { + return &inMemSequenceGenerator{ + nextValues: make(map[string]int), + } +} + +func (g *inMemSequenceGenerator) Reset() { + g.sequencesMu.Lock() + defer g.sequencesMu.Unlock() + + g.nextValues = make(map[string]int) +} + +func (g *inMemSequenceGenerator) Next(_ context.Context, table, column string) (int64, error) { + if table == "migration_log" { + // Don't use sequential IDs for migration log entries, as we don't clean up migration_log table between tests, + // so restarting the sequence can lead to conflicting IDs. + return rand.Int64(), nil + } + + key := fmt.Sprintf("%s:%s", table, column) + + g.sequencesMu.Lock() + defer g.sequencesMu.Unlock() + + seq, ok := g.nextValues[key] + if !ok { + seq = 1 + } + g.nextValues[key] = seq + 1 + return int64(seq), nil +} diff --git a/pkg/util/xorm/sequence_test.go b/pkg/util/xorm/sequence_test.go index 289e65e4034..b49bb568c8b 100644 --- a/pkg/util/xorm/sequence_test.go +++ b/pkg/util/xorm/sequence_test.go @@ -2,6 +2,7 @@ package xorm import ( "context" + "sync" "testing" "github.com/stretchr/testify/require" @@ -29,3 +30,97 @@ func TestSequenceGenerator(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(2), val) } + +func TestBatchSequenceAllocation(t *testing.T) { + eng, err := NewEngine("sqlite3", ":memory:") + require.NoError(t, err) + + _, err = eng.Exec("CREATE TABLE `autoincrement_sequences` (`name` STRING(128) NOT NULL PRIMARY KEY, `next_value` INT64 NOT NULL)") + require.NoError(t, err) + + // Create sequence generator with small batch size for testing + sg := newSequenceGenerator(eng.db.DB) + sg.SetBatchSize(10) + + // First batch (1-10) + for i := 1; i <= 10; i++ { + val, err := sg.Next(context.Background(), "test", "batch") + require.NoError(t, err) + require.Equal(t, int64(i), val) + } + + // Next value should trigger a new batch (11-20) + val, err := sg.Next(context.Background(), "test", "batch") + require.NoError(t, err) + require.Equal(t, int64(11), val) + + // Check database value is now set to next batch + var nextVal int64 + err = eng.db.QueryRow("SELECT next_value FROM autoincrement_sequences WHERE name = 'test:batch'").Scan(&nextVal) + require.NoError(t, err) + require.Equal(t, int64(21), nextVal, "Database should store the start of the next batch") + + // Continue getting values from the second batch + for i := 12; i <= 20; i++ { + val, err := sg.Next(context.Background(), "test", "batch") + require.NoError(t, err) + require.Equal(t, int64(i), val) + } +} + +func TestConcurrentSequenceAccess(t *testing.T) { + eng, err := NewEngine("sqlite3", ":memory:") + require.NoError(t, err) + + _, err = eng.Exec("CREATE TABLE `autoincrement_sequences` (`name` STRING(128) NOT NULL PRIMARY KEY, `next_value` INT64 NOT NULL)") + require.NoError(t, err) + + sg := newSequenceGenerator(eng.db.DB) + sg.SetBatchSize(100) + + // Launch multiple goroutines to get sequence values + const numRoutines = 50 + const valuesPerRoutine = 20 + results := make([]int64, numRoutines*valuesPerRoutine) + + var wg sync.WaitGroup + var mu sync.Mutex // To protect results slice + + ctx := context.Background() + + for i := 0; i < numRoutines; i++ { + wg.Add(1) + go func(routineID int) { + defer wg.Done() + + for j := 0; j < valuesPerRoutine; j++ { + val, err := sg.Next(ctx, "test", "concurrent") + require.NoError(t, err) + + // Store the result in our results array + mu.Lock() + results[routineID*valuesPerRoutine+j] = val + mu.Unlock() + } + }(i) + } + + wg.Wait() + + // Check that we have the expected number of values + require.Equal(t, numRoutines*valuesPerRoutine, len(results)) + + // Create a map to check for duplicates + seen := make(map[int64]bool) + for _, val := range results { + // Verify we haven't seen this value before + require.False(t, seen[val], "Found duplicate sequence value: %d", val) + seen[val] = true + } + + // Verify the range of values is correct (all values should be between 1 and numRoutines*valuesPerRoutine) + require.Equal(t, numRoutines*valuesPerRoutine, len(seen), "Should have exactly the right number of unique values") + for i := int64(1); i <= int64(numRoutines*valuesPerRoutine); i++ { + require.True(t, seen[i], "Missing sequence value: %d", i) + } +} diff --git a/pkg/util/xorm/xorm.go b/pkg/util/xorm/xorm.go index 25ce8bd952f..d1464af1da4 100644 --- a/pkg/util/xorm/xorm.go +++ b/pkg/util/xorm/xorm.go @@ -9,6 +9,7 @@ package xorm import ( "context" + "database/sql" "fmt" "os" "reflect" @@ -22,6 +23,8 @@ import ( const ( // Version show the xorm's version Version string = "0.8.0.1015" + + Spanner = "spanner" ) func regDrvsNDialects() bool { @@ -97,7 +100,7 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { switch uri.DbType { case core.SQLITE: engine.DatabaseTZ = time.UTC - case "spanner": + case Spanner: engine.DatabaseTZ = time.UTC // We need to specify "Z" to indicate that timestamp is in UTC. // Otherwise Spanner uses default America/Los_Angeles timezone. @@ -114,9 +117,30 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { runtime.SetFinalizer(engine, close) - if dialect.DBType() == "spanner" { - engine.sequenceGenerator = newSequenceGenerator(db.DB) + if ext, ok := dialect.(DialectExt); ok { + engine.sequenceGenerator, err = ext.CreateSequenceGenerator(db.DB) + if err != nil { + return nil, fmt.Errorf("failed to create sequence generator: %w", err) + } } return engine, nil } + +func (engine *Engine) ResetSequenceGenerator() { + if engine.sequenceGenerator != nil { + engine.sequenceGenerator.Reset() + } +} + +type SequenceGenerator interface { + Next(ctx context.Context, table, column string) (int64, error) + Reset() +} + +type DialectExt interface { + core.Dialect + + // CreateSequenceGenerator returns optional generator used to create AUTOINCREMENT ids for inserts. + CreateSequenceGenerator(db *sql.DB) (SequenceGenerator, error) +} diff --git a/public/api-merged.json b/public/api-merged.json index 6beecc94824..3ca4e15b213 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12654,6 +12654,9 @@ "isPaused": { "type": "boolean" }, + "keepFiringFor": { + "$ref": "#/definitions/Duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -12924,6 +12927,10 @@ "health": { "type": "string" }, + "keepFiringFor": { + "type": "number", + "format": "double" + }, "labels": { "$ref": "#/definitions/Labels" }, @@ -18688,9 +18695,22 @@ "Interval": { "$ref": "#/definitions/Duration" }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Limit": { + "type": "integer", + "format": "int64" + }, "Name": { "type": "string" }, + "QueryOffset": { + "type": "string" + }, "Rules": { "type": "array", "items": { @@ -18804,6 +18824,10 @@ "type": "boolean", "example": false }, + "keep_firing_for": { + "type": "string", + "format": "duration" + }, "labels": { "type": "object", "additionalProperties": { @@ -22869,7 +22893,6 @@ } }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", diff --git a/public/app/api/clients/provisioning/endpoints.gen.ts b/public/app/api/clients/provisioning/endpoints.gen.ts index 8f22200d13f..4be69764d5b 100644 --- a/public/app/api/clients/provisioning/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/endpoints.gen.ts @@ -1,11 +1,48 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Job', 'Repository', 'Provisioning'] as const; +export const addTagTypes = ['HistoricJob', 'Job', 'Repository', 'Provisioning'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + listHistoricJob: build.query({ + query: (queryArg) => ({ + url: `/historicjobs`, + params: { + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + pretty: queryArg.pretty, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['HistoricJob'], + }), + getHistoricJob: build.query({ + query: (queryArg) => ({ + url: `/historicjobs/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['HistoricJob'], + }), + getHistoricJobStatus: build.query({ + query: (queryArg) => ({ + url: `/historicjobs/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['HistoricJob'], + }), listJob: build.query({ query: (queryArg) => ({ url: `/jobs`, @@ -34,6 +71,15 @@ const injectedRtkApi = api }), providesTags: ['Job'], }), + getJobStatus: build.query({ + query: (queryArg) => ({ + url: `/jobs/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Job'], + }), listRepository: build.query({ query: (queryArg) => ({ url: `/repositories`, @@ -284,6 +330,65 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type ListHistoricJobApiResponse = /** status 200 OK */ HistoricJobList; +export type ListHistoricJobApiArg = { + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type GetHistoricJobApiResponse = /** status 200 OK */ HistoricJob; +export type GetHistoricJobApiArg = { + /** name of the HistoricJob */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type GetHistoricJobStatusApiResponse = /** status 200 OK */ HistoricJob; +export type GetHistoricJobStatusApiArg = { + /** name of the HistoricJob */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; export type ListJobApiResponse = /** status 200 OK */ JobList; export type ListJobApiArg = { /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ @@ -336,6 +441,13 @@ export type GetJobApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; }; +export type GetJobStatusApiResponse = /** status 200 OK */ Job; +export type GetJobStatusApiArg = { + /** name of the Job */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; export type ListRepositoryApiResponse = /** status 200 OK */ RepositoryList; export type ListRepositoryApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -497,7 +609,7 @@ export type CreateRepositoryExportApiArg = { /** Include the identifier in the exported metadata */ identifier: boolean; /** Prefix in target file system */ - prefix?: string; + path?: string; }; }; export type GetRepositoryFilesApiResponse = /** status 200 OK */ { @@ -587,8 +699,6 @@ export type CreateRepositoryMigrateApiArg = { history?: boolean; /** Include the identifier in the exported metadata */ identifier: boolean; - /** Target file prefix */ - prefix?: string; }; }; export type GetRepositoryRenderWithPathApiResponse = unknown; @@ -748,8 +858,6 @@ export type MigrateJobOptions = { history?: boolean; /** Include the identifier in the exported metadata */ identifier: boolean; - /** Target file prefix */ - prefix?: string; }; export type PullRequestJobOptions = { hash?: string; @@ -772,7 +880,7 @@ export type ExportJobOptions = { /** Include the identifier in the exported metadata */ identifier: boolean; /** Prefix in target file system */ - prefix?: string; + path?: string; }; export type JobSpec = { /** Possible enum values: @@ -823,7 +931,7 @@ export type JobStatus = { /** Summary of processed actions */ summary?: JobResourceSummary[]; }; -export type Job = { +export type HistoricJob = { /** 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; /** 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 */ @@ -842,6 +950,23 @@ export type ListMeta = { /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ selfLink?: string; }; +export type HistoricJobList = { + /** 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; + items?: HistoricJob[]; + /** 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?: ListMeta; +}; +export type Job = { + /** 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; + /** 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?: JobSpec; + status?: JobStatus; +}; export type JobList = { /** 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; @@ -1194,8 +1319,12 @@ export type ResourceStats = { metadata?: any; }; export const { + useListHistoricJobQuery, + useGetHistoricJobQuery, + useGetHistoricJobStatusQuery, useListJobQuery, useGetJobQuery, + useGetJobStatusQuery, useListRepositoryQuery, useCreateRepositoryMutation, useDeletecollectionRepositoryMutation, diff --git a/public/app/api/clients/provisioning/index.ts b/public/app/api/clients/provisioning/index.ts index b31f8c318cb..042b001e96a 100644 --- a/public/app/api/clients/provisioning/index.ts +++ b/public/app/api/clients/provisioning/index.ts @@ -8,6 +8,8 @@ import { JobList, Repository, RepositoryList, + HistoricJob, + HistoricJobList, } from './endpoints.gen'; import { createOnCacheEntryAdded } from './utils/createOnCacheEntryAdded'; @@ -22,6 +24,15 @@ export const provisioningAPI = generatedAPI.enhanceEndpoints({ }); endpoint.onCacheEntryAdded = createOnCacheEntryAdded('jobs'); }, + listHistoricJob(endpoint) { + endpoint.query = ({ watch, ...queryArg }) => ({ + url: `/historicjobs`, + params: queryArg, + }); + endpoint.onCacheEntryAdded = createOnCacheEntryAdded( + 'historicjobs' + ); + }, listRepository(endpoint) { endpoint.query = ({ watch, ...queryArg }) => ({ url: `/repositories`, diff --git a/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts b/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts index 724eb79193d..c3c4f38b8e1 100644 --- a/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts +++ b/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts @@ -48,17 +48,17 @@ export function createOnCacheEntryAdded< } const existingIndex = draft.items.findIndex((item) => item.metadata?.name === event.object.metadata.name); - if (event.type === 'ADDED') { - // Add the new item + if (event.type === 'ADDED' && existingIndex === -1) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions draft.items.push(event.object as unknown as T); - } else if (event.type === 'MODIFIED' && existingIndex !== -1) { - // Update the existing item if it exists - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - draft.items[existingIndex] = event.object as unknown as T; } else if (event.type === 'DELETED' && existingIndex !== -1) { // Remove the item if it exists draft.items.splice(existingIndex, 1); + } else if (existingIndex !== -1) { + // Could be ADDED or MODIFIED + // Update the existing item if it exists + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + draft.items[existingIndex] = event.object as unknown as T; } }); }); diff --git a/public/app/app.ts b/public/app/app.ts index da44a02a582..c0e73927896 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -43,7 +43,6 @@ import { import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; -import { setUseTranslateHook } from '@grafana/runtime/src/unstable'; import config, { updateConfig } from 'app/core/config'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -57,7 +56,7 @@ import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/componen import { PluginPage } from './core/components/Page/PluginPage'; import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext'; import { initializeCrashDetection } from './core/crash'; -import { initializeI18n, useTranslateInternal } from './core/internationalization'; +import { initializeI18n } from './core/internationalization'; import { setMonacoEnv } from './core/monacoEnv'; import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks'; import { CorrelationsService } from './core/services/CorrelationsService'; @@ -247,7 +246,6 @@ export class GrafanaApp { setReturnToPreviousHook(useReturnToPreviousInternal); setChromeHeaderHeightHook(useChromeHeaderHeight); - setUseTranslateHook(useTranslateInternal); if (config.featureToggles.crashDetection) { initializeCrashDetection(); diff --git a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap index 3bb1df8de0a..c0a9bc59c07 100644 --- a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -75,6 +75,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` }, "points": { "fill": [Function], + "one": true, "size": [Function], "stroke": [Function], "width": [Function], diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx new file mode 100644 index 00000000000..eecbd23f31f --- /dev/null +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -0,0 +1,23 @@ +import { Badge } from '@grafana/ui'; +import { ManagerKind } from 'app/features/apiserver/types'; +import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; +import { NestedFolderDTO } from 'app/features/search/service/types'; +import { FolderDTO, FolderListItemDTO } from 'app/types'; + +export interface Props { + folder?: FolderListItemDTO | NestedFolderDTO | FolderDTO; +} + +export function FolderRepo({ folder }: Props) { + const isProvisionedInstance = useIsProvisionedInstance(); + if ( + !folder || + ('parentUID' in folder && folder.parentUID) || + folder.managedBy !== ManagerKind.Repo || + isProvisionedInstance + ) { + return null; + } + + return ; +} diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index 5dd67667c33..12ec2d57975 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -14,6 +14,8 @@ import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardViewItem } from 'app/features/search/types'; import { useSelector } from 'app/types'; +import { FolderRepo } from './FolderRepo'; + const ROW_HEIGHT = 40; const CHEVRON_SIZE = 'md'; @@ -212,6 +214,7 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
@@ -275,6 +278,9 @@ const getStyles = (theme: GrafanaTheme2) => { rowBody, label: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), lineHeight: ROW_HEIGHT + 'px', flexGrow: 1, minWidth: 0, diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index f5c90e5f403..226583c54e1 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -42,6 +42,12 @@ describe('NestedFolderPicker', () => { }); }), + http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => { + return HttpResponse.json({ + items: [], + }); + }), + http.get('/api/folders', ({ request }) => { const url = new URL(request.url); const parentUid = url.searchParams.get('parentUid') ?? undefined; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 2656b96b6a2..83eedefef29 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Alert, Icon, Input, LoadingBar, useStyles2 } from '@grafana/ui'; +import { Alert, Icon, Input, LoadingBar, Stack, Text, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { skipToken, useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; @@ -16,6 +16,7 @@ import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; import { PermissionLevelString } from 'app/types'; +import { FolderRepo } from './FolderRepo'; import { getDOMId, NestedFolderList } from './NestedFolderList'; import Trigger from './Trigger'; import { ROOT_FOLDER_ITEM, useFoldersQuery } from './useFoldersQuery'; @@ -266,10 +267,20 @@ export function NestedFolderPicker({ label = 'Dashboards'; } + // Display the folder name and provisioning status when the picker is closed + const labelComponent = label ? ( + + {label} + + + ) : ( + '' + ); + if (!overlayOpen) { return ( { const folderIsOpen = openFolders[item.uid]; - const flatItem: DashboardsTreeItem = { isOpen: Boolean(folderIsOpen), level: level, @@ -154,6 +153,7 @@ export function useFoldersQuery( kind: 'folder' as const, title: item.title, uid: item.uid, + managedBy: item.managedBy, }, }; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index a7dcaf1e293..d4e4d669841 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -120,6 +120,11 @@ export class SharedPreferences extends PureComponent { if (confirmationResult) { const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar } = this.state; + reportInteraction('grafana_preferences_save_button_clicked', { + preferenceType: this.props.preferenceType, + theme, + language, + }); await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar }); window.location.reload(); } diff --git a/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx b/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx index a633bdb3f53..32a2e869b90 100644 --- a/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx +++ b/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx @@ -22,7 +22,7 @@ export function ThemeSelectorDrawer({ onClose }: Props) { const onChange = (theme: ThemeRegistryItem) => { reportInteraction('grafana_preferences_theme_changed', { toTheme: theme.id, - preferenceType: 'user', + preferenceType: 'theme_drawer', }); changeTheme(theme.id, false); }; diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 1d6b445149b..5328be7e863 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -576,6 +576,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ focus: { prox: hoverProximity ?? DEFAULT_FOCUS_PROXIMITY, }, + points: { one: true }, }; builder.setCursor(cursor); diff --git a/public/app/core/internationalization/index.test.tsx b/public/app/core/internationalization/index.test.tsx index 8eabddb6f15..8dab4d6e250 100644 --- a/public/app/core/internationalization/index.test.tsx +++ b/public/app/core/internationalization/index.test.tsx @@ -1,6 +1,41 @@ import { render } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; -import { Trans } from './index'; +import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; +import { + Trans as PluginTrans, + setTransComponent, + setUseTranslateHook, + useTranslate, +} from '@grafana/runtime/src/unstable'; + +import { getI18next, Trans, useTranslateInternal } from './index'; + +const id = 'frontend-test-locales-plugin'; +const mockedMeta: PluginMeta = { + id, + name: 'Frontend Test Locales Plugin', + type: PluginType.panel, + info: { + author: { name: 'Test Author' }, + description: 'Test Description', + links: [], + logos: { + large: 'test-plugin-large-logo', + small: 'test-plugin-small-logo', + }, + screenshots: [], + version: '1.0.0', + updated: '2021-01-01', + }, + module: 'test-plugin', + baseUrl: 'test-plugin', +}; + +const DummyUseTranslateComponent = () => { + const t = useTranslate(); + return
{t('frontendtests.test-key', 'test-key not found')}
; +}; describe('internationalization', () => { describe('Trans component', () => { @@ -22,4 +57,59 @@ describe('internationalization', () => { expect(getByText('Table - <script></script>')).toBeInTheDocument(); }); }); + describe('for plugins', () => { + beforeEach(() => { + getI18next().addResourceBundle('en', id, { 'frontendtests.test-key': 'test-value' }, undefined, true); + setTransComponent(Trans); + setUseTranslateHook(useTranslateInternal); + }); + + it('should return the correct value when using Trans component within a plugin context', async () => { + const { getByText, queryByText } = render( + + + + + + ); + + expect(getByText('test-value')).toBeInTheDocument(); + expect(queryByText('test-key not found')).not.toBeInTheDocument(); + }); + + it('should return the correct value when using Trans component without a plugin context', async () => { + const { getByText, queryByText } = render( + + + + ); + + expect(getByText('test-key not found')).toBeInTheDocument(); + expect(queryByText('test-value')).not.toBeInTheDocument(); + }); + + it('should return the correct value when using useTranslate hook within a plugin context', async () => { + const { getByText, queryByText } = render( + + + + + + ); + + expect(getByText('test-value')).toBeInTheDocument(); + expect(queryByText('test-key not found')).not.toBeInTheDocument(); + }); + + it('should return the correct value when using useTranslate hook without a plugin context', async () => { + const { getByText, queryByText } = render( + + + + ); + + expect(getByText('test-key not found')).toBeInTheDocument(); + expect(queryByText('test-value')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index 821887eb53a..73242d04728 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -1,8 +1,11 @@ import i18n, { InitOptions, TFunction } from 'i18next'; import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetector'; -import { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports +import { usePluginContext } from '@grafana/data'; +import { setTransComponent, setUseTranslateHook, TransProps } from '@grafana/runtime/src/unstable'; + import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants'; import { loadTranslations } from './loadTranslations'; @@ -59,6 +62,9 @@ export async function initializeI18n(language: string): Promise<{ language: stri tFunc = i18n.getFixedT(null, NAMESPACES); + setUseTranslateHook(useTranslateInternal); + setTransComponent(Trans); + return { language: i18nInstance.resolvedLanguage, }; @@ -69,14 +75,14 @@ export function changeLanguage(locale: string) { return i18n.changeLanguage(validLocale); } -type I18NextTransType = typeof I18NextTrans; -type I18NextTransProps = Parameters[0]; - -interface TransProps extends I18NextTransProps { - i18nKey: string; -} - export const Trans = (props: TransProps): ReactElement => { + const context = usePluginContext(); + + // If we are in a plugin context, use the plugin's id as the namespace + if (context?.meta?.id) { + return ; + } + return ; }; @@ -131,5 +137,12 @@ export function getI18next() { // Perhaps in the future this will use useTranslation from react-i18next or something else // from context export function useTranslateInternal() { - return t; + const context = usePluginContext(); + if (!context) { + return t; + } + + const { meta } = context; + const pluginT = useMemo(() => getI18next().getFixedT(null, meta.id), [meta.id]); + return pluginT; } diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 815b96815c8..bc3690d43c3 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -7,7 +7,7 @@ import { NavModelItem } from '@grafana/data'; import { Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; import { OrgUsersTable } from './Users/OrgUsersTable'; @@ -56,7 +56,10 @@ const AdminEditOrgPage = () => { }; const renderMissingPermissionMessage = () => ( - + You do not have permission to see users in this organization. To update this organization, contact your server administrator. @@ -93,7 +96,12 @@ const AdminEditOrgPage = () => { {orgState.value && (
- + -
GA
+
+ GA +
); case 'privatePreview': case 'preview': case 'experimental': - return 'Beta'; + return t('admin.admin-feature-toggles-table.get-stage-cell.beta', 'Beta'); case 'deprecated': - return 'Deprecated'; + return t('admin.admin-feature-toggles-table.get-stage-cell.deprecated', 'Deprecated'); default: return stage; } @@ -156,7 +159,10 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat

diff --git a/public/app/features/admin/AdminOrgsTable.tsx b/public/app/features/admin/AdminOrgsTable.tsx index 02a2de9cc6b..e842150f31c 100644 --- a/public/app/features/admin/AdminOrgsTable.tsx +++ b/public/app/features/admin/AdminOrgsTable.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Button, ConfirmModal, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { contextSrv } from 'app/core/core'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { AccessControlAction, Organization } from 'app/types'; interface Props { @@ -51,7 +51,7 @@ function AdminOrgsTableComponent({ orgs, onDelete }: Props) { size="sm" icon="times" onClick={() => setDeleteOrg(org)} - aria-label="Delete org" + aria-label={t('admin.admin-orgs-table.aria-label-delete-org', 'Delete org')} disabled={!canDeleteOrgs} /> @@ -62,7 +62,7 @@ function AdminOrgsTableComponent({ orgs, onDelete }: Props) { diff --git a/public/app/features/admin/OrgRolePicker.tsx b/public/app/features/admin/OrgRolePicker.tsx index 3e637d05e0a..1c89a6fe74b 100644 --- a/public/app/features/admin/OrgRolePicker.tsx +++ b/public/app/features/admin/OrgRolePicker.tsx @@ -1,5 +1,6 @@ import { OrgRole } from '@grafana/data'; import { Select } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; interface Props { value: OrgRole; @@ -21,7 +22,7 @@ export function OrgRolePicker({ value, onChange, 'aria-label': ariaLabel, inputI value={value} options={options} onChange={(val) => onChange(val.value ?? OrgRole.None)} - placeholder="Choose role..." + placeholder={t('admin.org-role-picker.placeholder-choose-role', 'Choose role...')} aria-label={ariaLabel} autoFocus={autoFocus} {...restProps} diff --git a/public/app/features/admin/UpgradePage.tsx b/public/app/features/admin/UpgradePage.tsx index 2ba5d8476bc..d9873f2ed0d 100644 --- a/public/app/features/admin/UpgradePage.tsx +++ b/public/app/features/admin/UpgradePage.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { GrafanaTheme2, NavModel } from '@grafana/data'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; @@ -110,9 +110,18 @@ const ServiceInfo = () => { - - - + + + 24 x 7 x 365 support via @@ -154,26 +163,34 @@ const FeatureInfo = () => { const FeatureListing = () => { return ( - - - - - + + + + + LDAP, GitHub OAuth, Auth Proxy, Okta - - - - + + + + - - - - + + + + - + + {/* eslint-disable @grafana/no-untranslated-strings */} @@ -189,6 +206,7 @@ const FeatureListing = () => { + {/* eslint-enable @grafana/no-untranslated-strings */} diff --git a/public/app/features/admin/UserCreatePage.tsx b/public/app/features/admin/UserCreatePage.tsx index b4f22b4c071..4c08caa2825 100644 --- a/public/app/features/admin/UserCreatePage.tsx +++ b/public/app/features/admin/UserCreatePage.tsx @@ -6,7 +6,7 @@ import { NavModelItem } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { Button, Input, Field } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; interface UserDTO { name: string; @@ -45,19 +45,24 @@ const UserCreatePage = () => { - + - + - + { - + diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index 5263019b883..827eb178619 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -75,7 +75,10 @@ const UserListAdminPageUnConnected = ({

setView(TabView.ADMIN)} data-testid={selectors.tabs.allUsers} /> setView(TabView.ORG)} data-testid={selectors.tabs.orgUsers} /> {config.anonymousEnabled && ( setView(TabView.ANON)} data-testid={selectors.tabs.anonUserDevices} @@ -94,7 +94,7 @@ export default function UserListPage() { isEmailSharingEnabled() && ( setView(TabView.ORG)} data-testid={selectors.tabs.users} diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 2e384aa51de..de65bdca315 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -363,14 +363,14 @@ export class AddToOrgModal extends PureComponent - + - + - + { )} ) : ( onRoleChange(newRole, original)} @@ -254,7 +254,7 @@ export const OrgUsersTable = ({ { setUserToRemove(null); }} diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx index c397dccf6b7..23fbf4b763f 100644 --- a/public/app/features/admin/Users/UsersTable.tsx +++ b/public/app/features/admin/Users/UsersTable.tsx @@ -16,7 +16,7 @@ import { Tooltip, } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { UserDTO } from 'app/types'; import { OrgUnits } from './OrgUnits'; @@ -55,7 +55,12 @@ export const UsersTable = ({ header: 'Login', cell: ({ row: { original } }: Cell<'login'>) => { return ( - + {original.login} ); diff --git a/public/app/features/admin/ldap/LdapConnectionStatus.tsx b/public/app/features/admin/ldap/LdapConnectionStatus.tsx index 281013fe76b..aa1d608a9c8 100644 --- a/public/app/features/admin/ldap/LdapConnectionStatus.tsx +++ b/public/app/features/admin/ldap/LdapConnectionStatus.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { Alert, CellProps, Column, Icon, InteractiveTable, Stack, Text, Tooltip } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { AppNotificationSeverity, LdapConnectionInfo, LdapServerInfo } from 'app/types'; interface Props { @@ -98,7 +98,10 @@ export const LdapErrorBox = ({ ldapConnectionInfo }: LdapConnectionErrorProps) = )); return ( - + {errorElements} ); diff --git a/public/app/features/admin/ldap/LdapDrawer.tsx b/public/app/features/admin/ldap/LdapDrawer.tsx index 0533fadb634..21e49aa725b 100644 --- a/public/app/features/admin/ldap/LdapDrawer.tsx +++ b/public/app/features/admin/ldap/LdapDrawer.tsx @@ -144,6 +144,7 @@ export const LdapDrawerComponent = ({ > Test user mapping - + { */ if (!config.featureToggles.ssoSettingsLDAP) { return ( - + This page is only accessible by enabling the ssoSettingsLDAP feature flag. @@ -421,14 +421,21 @@ export const LdapSettingsPage = () => { - + } placement="bottom-start" > { header: 'Role', cell: (props: CellProps) => props.value || ( - <> - - No match{' '} - - - - - + + No match + + + + ), }, ], diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 9bcb8f7312f..5aa3099965c 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -91,7 +91,7 @@ export const AlertsFolderView = ({ folder }: Props) => { {pageItems.map((currentRule) => ( Select a contact point to notify all recipients in it.
+ Muting, grouping, and timings options allow you to customize how notifications are sent.
- Notifications for firing alert instances are grouped based on folder and alert rule name.
- The wait time before sending the first notification for a new group of alerts is 30 seconds. -
- The waiting time before sending a notification about changes in the alert group after the first notification - has been sent is 5 minutes. -
- The wait time before resending a notification that has already been sent successfully is 4 hours. -
- Grouping and wait time values are defined in your default notification policy. + Alternatively, toggle the Advanced options button to route notifications using notification policies + for greater flexibility. } externalLink="https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/" linkText="Read more about notifications" - title="Notify contact points" + title="Notify by selecting a contact point" /> ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index 08c67ebf3d2..4e951c5110a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -14,6 +14,7 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, Card, Icon, Stack } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; +import { isExpressionQuery } from 'app/features/expressions/guards'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; @@ -236,8 +237,10 @@ function copyModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit } function newModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit { - const isInstant = getInstantFromDataQuery(item.model, settings.type); - return { + const isExpression = isExpressionQuery(item); + const isInstant = isExpression ? false : getInstantFromDataQuery(item); + + const newQuery: Omit = { refId: item.refId, relativeTimeRange: item.relativeTimeRange, queryType: '', @@ -246,9 +249,14 @@ function newModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit< refId: item.refId, hide: false, datasource: getDataSourceRef(settings), - instant: isInstant, }, }; + + if (isInstant && !isExpressionQuery(item)) { + (newQuery as AlertQuery).model.instant = isInstant; + } + + return newQuery; } interface DatasourceNotFoundProps { diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index b99fcac732a..f68296b6fbb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -320,7 +320,7 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) => {/* Step 1 */} {/* Step 2 */} - + {/* Step 3-4-5 */} {showDataSourceDependantStep && ( <> diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 8b867293622..32e4fd72f13 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -100,7 +100,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor {/* Step 1 */} {/* Step 2 */} - + {/* Step 3-4-5 */} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx index de3cb8bfcc8..3d86b3e33cb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx @@ -3,12 +3,14 @@ import { useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { CollapsableSection, Stack, useStyles2 } from '@grafana/ui'; +import { CollapsableSection, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource'; import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints'; import { ContactPointWithMetadata } from '../../../contact-points/utils'; +import { NeedHelpInfo } from '../../NeedHelpInfo'; import { ContactPointDetails } from './contactPoint/ContactPointDetails'; import { ContactPointSelector } from './contactPoint/ContactPointSelector'; @@ -57,7 +59,7 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
- Alertmanager: + Alertmanager: Alert manager logo {alertManagerName}
@@ -74,8 +76,37 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo label="Muting, grouping and timings (optional)" isOpen={hasRouteSettings} className={styles.collapsableSection} + contentClassName={styles.collapsableSectionContent} > + + + + Configure how notifications for this alert rule are sent. + + + +

+ {t( + 'alerting.rule-form.simple-routing.optional-settings.help-info1', + 'Mute timings allows you to temporarily pause notifications for a specific recurring period, such as a regular maintenance window or weekends.' + )} +

+ {t( + 'alerting.rule-form.simple-routing.optional-settings.help-info2', + 'Grouping and timing options combine multiple alerts within a specific period into a single notification, allowing you to customize default options.' + )} + + } + /> +
@@ -110,6 +141,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ width: 'fit-content', fontSize: theme.typography.body.fontSize, }), + collapsableSectionContent: css({ + padding: '0', + }), routingSection: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx index 86608e72112..0a7810a37a4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx @@ -1,6 +1,8 @@ +import { css } from '@emotion/css'; import { Controller, useFormContext } from 'react-hook-form'; -import { Field } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Field, useStyles2 } from '@grafana/ui'; import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector'; import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; @@ -8,6 +10,7 @@ import { mapMultiSelectValueToStrings } from 'app/features/alerting/unified/util /** Provides a form field for use in simplified routing, for selecting appropriate mute timings */ export function MuteTimingFields({ alertmanager }: BaseAlertmanagerArgs) { + const styles = useStyles2(getStyles); const { control, formState: { errors }, @@ -18,6 +21,7 @@ export function MuteTimingFields({ alertmanager }: BaseAlertmanagerArgs) { label="Mute timings" data-testid="am-mute-timing-select" description="Select a mute timing to define when not to send notifications for this alert rule" + className={styles.muteTimingField} invalid={!!errors.contactPoints?.[alertmanager]?.muteTimeIntervals} > ); } +const getStyles = (theme: GrafanaTheme2) => ({ + muteTimingField: css({ + marginTop: theme.spacing(1), + }), +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 20d3359a03e..463401721a0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -23,7 +23,12 @@ import { } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; +import { + ExpressionDatasourceUID, + ExpressionQuery, + ExpressionQueryType, + expressionTypes, +} from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; @@ -50,7 +55,7 @@ import { RuleEditorSection } from '../RuleEditorSection'; import { errorFromCurrentCondition, errorFromPreviewData, findRenamedDataQueryReferences, refIdExists } from '../util'; import { CloudDataSourceSelector } from './CloudDataSourceSelector'; -import { SimpleConditionEditor, SimpleConditionIdentifier, getSimpleConditionFromExpressions } from './SimpleCondition'; +import { SimpleConditionEditor, getSimpleConditionFromExpressions } from './SimpleCondition'; import { SmartAlertTypeDetector } from './SmartAlertTypeDetector'; import { DESCRIPTIONS } from './descriptions'; import { @@ -77,9 +82,15 @@ import { useAlertQueryRunner } from './useAlertQueryRunner'; interface Props { editingExistingRule: boolean; onDataChange: (error: string) => void; + /** + * The mode of the rule editor. + * - 'edit' standard rule editor mode + * - 'draft' non-saveable form mode used for exporting to provisioning formats + */ + mode: 'edit' | 'draft'; } -export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: Props) => { +export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mode }: Props) => { const { setValue, getValues, @@ -140,7 +151,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P ); const simplifiedQueryStep = - isSwitchModeEnabled && isGrafanaAlertingType ? getValues('editorSettings.simplifiedQueryEditor') : false; + isSwitchModeEnabled && isGrafanaAlertingType ? editorSettings?.simplifiedQueryEditor : false; // If we switch to simple mode we need to update the simple condition with the data in the queries reducer useEffect(() => { @@ -158,15 +169,22 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P // Grafana Managed rules and recording rules do return; } - // we need to be sure the condition is set once we switch to simple mode + if (simplifiedQueryStep) { - setValue('condition', SimpleConditionIdentifier.thresholdId); - runQueries(getValues('queries'), SimpleConditionIdentifier.thresholdId); + const lastExpression = expressionQueries.at(-1); + if (!lastExpression) { + return; + } + + const condition = lastExpression.refId; + // we need to be sure the condition is set once we switch to simple mode + setValue('condition', condition); + runQueries(getValues('queries'), condition); } else { runQueries(getValues('queries'), condition || (getValues('condition') ?? '')); } }, - [isCloudAlertRuleType, runQueries, getValues, simplifiedQueryStep, setValue] + [isCloudAlertRuleType, expressionQueries, simplifiedQueryStep, setValue, runQueries, getValues] ); // whenever we update the queries we have to update the form too @@ -241,7 +259,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P // As a workaround we update form values as soon as possible to avoid stale state // This way we can access up to date queries in runQueriesPreview without waiting for re-render const previousQueries = getValues('queries'); - const expressionQueries = previousQueries.filter((query) => isExpressionQuery(query.model)); + + const expressionQueries = previousQueries.filter>(isExpressionQueryInAlert); + setValue('queries', [...updatedQueries, ...expressionQueries], { shouldValidate: false }); updateExpressionAndDatasource(updatedQueries); @@ -515,12 +535,14 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P }} /> - + {mode === 'edit' && ( + + )} )} @@ -555,7 +577,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P )} {/* We only show Switch for Grafana managed alerts */} - {isGrafanaAlertingType && !simplifiedQueryStep && ( + {isGrafanaAlertingType && !simplifiedQueryStep && mode === 'edit' && ( >, dispatch: Dispatch ) { - const reduceExpression = expressionQueriesList.find( - (query) => - query.model.type === ExpressionQueryType.reduce && query.model.refId === SimpleConditionIdentifier.reducerId - ); + // 1. make sure have have a reduce expression and that it is pointing to the data query + const reduceExpression = expressionQueriesList.find((query) => query.model.type === ExpressionQueryType.reduce); const newReduceExpression = reduceExpression ? produce(reduceExpression?.model, (draft) => { @@ -179,10 +172,7 @@ function updateThresholdFunction( expressionQueriesList: Array>, dispatch: Dispatch ) { - const thresholdExpression = expressionQueriesList.find( - (query) => - query.model.type === ExpressionQueryType.threshold && query.model.refId === SimpleConditionIdentifier.thresholdId - ); + const thresholdExpression = expressionQueriesList.find((query) => query.model.type === ExpressionQueryType.threshold); const newThresholdExpression = produce(thresholdExpression, (draft) => { if (draft && draft.model.conditions) { @@ -198,10 +188,7 @@ function updateThresholdValue( expressionQueriesList: Array>, dispatch: Dispatch ) { - const thresholdExpression = expressionQueriesList.find( - (query) => - query.model.type === ExpressionQueryType.threshold && query.model.refId === SimpleConditionIdentifier.thresholdId - ); + const thresholdExpression = expressionQueriesList.find((query) => query.model.type === ExpressionQueryType.threshold); const newThresholdExpression = produce(thresholdExpression, (draft) => { if (draft && draft.model.conditions) { @@ -212,13 +199,8 @@ function updateThresholdValue( } export function getSimpleConditionFromExpressions(expressions: Array>): SimpleCondition { - const reduceExpression = expressions.find( - (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId - ); - const thresholdExpression = expressions.find( - (query) => - query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId - ); + const reduceExpression = expressions.find((query) => query.model.type === ExpressionQueryType.reduce); + const thresholdExpression = expressions.find((query) => query.model.type === ExpressionQueryType.threshold); const conditionsFromThreshold = thresholdExpression?.model.conditions ?? []; const whenField = reduceExpression?.model.reducer; const params = conditionsFromThreshold[0]?.evaluator?.params diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts index 2a9294ada7d..96be5b02dc5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts @@ -1,35 +1,50 @@ import { produce } from 'immer'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; -import { dataQuery, reduceExpression, thresholdExpression } from 'app/features/alerting/unified/mocks'; +import { + mockDataQuery, + mockDataSource, + mockReduceExpression, + mockThresholdExpression, +} from 'app/features/alerting/unified/mocks'; import { areQueriesTransformableToSimpleCondition } from 'app/features/alerting/unified/rule-editor/formProcessing'; +import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; +import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; import { ExpressionQuery, ReducerMode } from 'app/features/expressions/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -const expressionQueries: Array> = [reduceExpression, thresholdExpression]; +const reduceExpression = mockReduceExpression({ expression: 'A', settings: { mode: ReducerMode.Strict } }); +const thresholdExpression = mockThresholdExpression({ expression: 'B' }); +const expressionQueries: Array> = [reduceExpression, thresholdExpression]; +const ds = mockDataSource({ type: DataSourceType.Prometheus, name: 'Mimir-cloud', uid: 'abc123' }); describe('areQueriesTransformableToSimpleCondition', () => { + beforeEach(() => { + setupDataSources(ds); + }); it('should return false if dataQueries length is not 1', () => { // zero dataQueries expect(areQueriesTransformableToSimpleCondition([], expressionQueries)).toBe(false); // more than one dataQueries - expect(areQueriesTransformableToSimpleCondition([dataQuery, dataQuery], expressionQueries)).toBe(false); + expect(areQueriesTransformableToSimpleCondition([mockDataQuery(), mockDataQuery()], expressionQueries)).toBe(false); }); + it('should return false if expressionQueries length is not 2', () => { - const dataQueries: Array> = [dataQuery]; + const dataQueries: Array> = [mockDataQuery()]; const result = areQueriesTransformableToSimpleCondition(dataQueries, []); expect(result).toBe(false); }); - it('should return false if the dataQuery refId does not match SimpleConditionIdentifier.queryId', () => { - const dataQueries: Array> = [ - { refId: 'notSimpleCondition', datasourceUid: 'abc123', queryType: '', model: { refId: 'notSimpleCondition' } }, - ]; + // notSimpleCondition + // reducer: + it('should return false if the mockDataQuery() refId does not match SimpleConditionIdentifier.queryId', () => { + const dataQueries: Array> = [mockDataQuery({ refId: 'foo' })]; const result = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); expect(result).toBe(false); }); + it('should return false if no reduce expression is found with correct type and refId', () => { - const dataQueries: Array> = [dataQuery]; + const dataQueries: Array> = [mockDataQuery()]; const result = areQueriesTransformableToSimpleCondition(dataQueries, [ { ...reduceExpression, refId: 'hello' }, thresholdExpression, @@ -37,17 +52,25 @@ describe('areQueriesTransformableToSimpleCondition', () => { expect(result).toBe(false); }); - it('should return false if no threshold expression is found with correct type and refId', () => { - const dataQueries: Array> = [dataQuery]; + it('should return false if no threshold expression is found that points to reducer', () => { + const dataQueries: Array> = [mockDataQuery()]; const result = areQueriesTransformableToSimpleCondition(dataQueries, [ reduceExpression, - { ...thresholdExpression, refId: 'hello' }, + mockThresholdExpression({ expression: 'hello' }), + ]); + expect(result).toBe(false); + }); + + it('should return false if no threshold expression is found that points to instant data query', () => { + const dataQueries: Array> = [mockDataQuery({ instant: true })]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, [ + mockThresholdExpression({ expression: 'hello' }), ]); expect(result).toBe(false); }); it('should return false if reduceExpression settings mode is not ReducerMode.Strict', () => { - const dataQueries: Array> = [dataQuery]; + const dataQueries: Array> = [mockDataQuery()]; const transformedReduceExpression = produce(reduceExpression, (draft) => { draft.model.settings = { mode: ReducerMode.DropNonNumbers }; }); @@ -60,7 +83,7 @@ describe('areQueriesTransformableToSimpleCondition', () => { }); it('should return false if thresholdExpression unloadEvaluator has a value', () => { - const dataQueries: Array> = [dataQuery]; + const dataQueries: Array> = [mockDataQuery()]; const transformedThresholdExpression = produce(thresholdExpression, (draft) => { draft.model.conditions = [ @@ -79,9 +102,17 @@ describe('areQueriesTransformableToSimpleCondition', () => { ]); expect(result).toBe(false); }); - it('should return true when all conditions are met', () => { - const dataQueries: Array> = [dataQuery]; - const result = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); + + it('should return true when data query is connected to valid reducer and threshold', () => { + const result = areQueriesTransformableToSimpleCondition([mockDataQuery({ refId: 'A' })], expressionQueries); + expect(result).toBe(true); + }); + + it('should return true when all conditions are met for instant data query with threshold', () => { + const result = areQueriesTransformableToSimpleCondition( + [mockDataQuery({ instant: true })], + [mockThresholdExpression({ expression: 'A' })] + ); expect(result).toBe(true); }); }); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap index a316611bf55..292a6904f80 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap @@ -130,16 +130,16 @@ exports[`Query and expressions reducer should add reduce expression if there is }, "expression": "A", "reducer": "last", - "refId": "B", + "refId": "reducer", "type": "reduce", }, "queryType": "expression", - "refId": "B", + "refId": "reducer", }, { "datasourceUid": "__expr__", "model": { - "expression": "B", + "expression": "reducer", "refId": "C", "type": "threshold", }, @@ -218,6 +218,31 @@ exports[`Query and expressions reducer should remove first reducer 1`] = ` } `; +exports[`Query and expressions reducer should remove reducer even if reducer is not the first expression 1`] = ` +{ + "queries": [ + { + "datasourceUid": "abc123", + "model": { + "refId": "A", + }, + "queryType": "query", + "refId": "A", + }, + { + "datasourceUid": "__expr__", + "model": { + "expression": "A", + "refId": "C", + "type": "threshold", + }, + "queryType": "expression", + "refId": "C", + }, + ], +} +`; + exports[`Query and expressions reducer should rewire expressions 1`] = ` { "queries": [ diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx index 2a112e089c3..73372e12a57 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx @@ -10,7 +10,6 @@ import { import { defaultCondition } from 'app/features/expressions/utils/expressionTypes'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { SimpleConditionIdentifier } from './SimpleCondition'; import { QueriesAndExpressionsState, addNewDataQuery, @@ -28,22 +27,23 @@ import { } from './reducer'; const reduceExpression: AlertQuery = { - refId: SimpleConditionIdentifier.reducerId, + refId: 'B', queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.reduce, - refId: SimpleConditionIdentifier.reducerId, + refId: 'B', settings: { mode: ReducerMode.Strict }, + expression: 'A', }, }; const thresholdExpression: AlertQuery = { - refId: SimpleConditionIdentifier.thresholdId, + refId: 'C', queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.threshold, - refId: SimpleConditionIdentifier.thresholdId, + refId: 'C', }, }; @@ -400,7 +400,7 @@ describe('Query and expressions reducer', () => { expect(newState).toMatchSnapshot(); }); - it('should not remove first reducer if reducer is not the first expression', () => { + it('should remove reducer even if reducer is not the first expression', () => { const initialState: QueriesAndExpressionsState = { queries: [alertQuery, thresholdExpression, reduceExpression], }; @@ -412,7 +412,7 @@ describe('Query and expressions reducer', () => { expressionQueries: [thresholdExpression, reduceExpression], }) ); - expect(newState).toEqual(initialState); + expect(newState).toMatchSnapshot(); }); it('should not remove first reducer if reducer is not the second query', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index aab9e184a56..b3499b8c520 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -8,21 +8,25 @@ import { getNextRefId, rangeUtil, } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; -import { defaultCondition } from 'app/features/expressions/utils/expressionTypes'; +import { + defaultCondition, + isReducerExpression, + isThresholdExpression, +} from 'app/features/expressions/utils/expressionTypes'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { logError } from '../../../Analytics'; -import { DataSourceType, getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; +import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; import { getDefaultQueries, getInstantFromDataQuery } from '../../../utils/rule-form'; import { createDagFromQueries, getOriginOfRefId } from '../dag'; import { queriesWithUpdatedReferences, refIdExists } from '../util'; -import { SimpleConditionIdentifier } from './SimpleCondition'; +// this one will be used as the refID when we create a new reducer for the threshold expression +export const NEW_REDUCER_REF = 'reducer'; export interface QueriesAndExpressionsState { queries: AlertQuery[]; @@ -64,9 +68,10 @@ export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: export const updateMinInterval = createAction<{ refId: string; minInterval: string }>('updateMinInterval'); export const resetToSimpleCondition = createAction('resetToSimpleCondition'); -export const optimizeReduceExpression = createAction<{ updatedQueries: AlertQuery[]; expressionQueries: AlertQuery[] }>( - 'optimizeReduceExpression' -); +export const optimizeReduceExpression = createAction<{ + updatedQueries: AlertQuery[]; + expressionQueries: Array>; +}>('optimizeReduceExpression'); export const setRecordingRulesQueries = createAction<{ recordingRuleQueries: AlertQuery[]; expression: string }>( 'setRecordingRulesQueries' ); @@ -231,6 +236,7 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder .addCase(rewireExpressions, (state, { payload }) => { state.queries = queriesWithUpdatedReferences(state.queries, payload.oldRefId, payload.newRefId); }) + // removes the reduce expression when we have a instant data query .addCase(optimizeReduceExpression, (state, { payload }) => { const { updatedQueries, expressionQueries } = payload; @@ -239,48 +245,29 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder return; } - //sometimes we dont have data source in the model yet - const getDataSourceSettingsForFirstQuery = getDataSourceSrv().getInstanceSettings( - updatedQueries[0].datasourceUid - ); - - if (!getDataSourceSettingsForFirstQuery) { - return; - } - const type = getDataSourceSettingsForFirstQuery?.type; - - const firstQueryIsPromOrLoki = type === DataSourceType.Prometheus || type === DataSourceType.Loki; - - const isInstant = getInstantFromDataQuery(updatedQueries[0].model, type); - - const shouldRemoveReducer = - firstQueryIsPromOrLoki && updatedQueries.length === 1 && isInstant && expressionQueries.length === 2; - - const onlyOneExpressionNotReducer = - expressionQueries.length === 1 && - 'type' in expressionQueries[0].model && - expressionQueries[0].model.type !== ExpressionQueryType.reduce; - - // we only add the reduce expression if we have one data query and one expression query. For other cases we don't do anything, - // and let the user add the reducer manually. - const shouldAddReduceExpression = - firstQueryIsPromOrLoki && updatedQueries.length === 1 && !isInstant && onlyOneExpressionNotReducer; + const dataQuery = updatedQueries.at(0); + const isInstantDataQuery = dataQuery ? getInstantFromDataQuery(dataQuery) : false; + const shouldRemoveReducer = isInstantDataQuery && expressionQueries.length === 2; if (shouldRemoveReducer) { const reduceExpressionIndex = state.queries.findIndex( - (query) => isExpressionQuery(query.model) && query.model.type === ExpressionQueryType.reduce + (query) => + isExpressionQuery(query.model) && + isReducerExpression(query.model) && + query.model.expression === dataQuery?.refId ); - if (reduceExpressionIndex === 1) { - // means the reduce expression is the second query - state.queries.splice(reduceExpressionIndex, 1); - state.queries[1].model.expression = SimpleConditionIdentifier.queryId; - } + state.queries.splice(reduceExpressionIndex, 1); + state.queries[1].model.expression = dataQuery?.refId; } + + const shouldAddReduceExpression = + !isInstantDataQuery && expressionQueries.length === 1 && isThresholdExpression(expressionQueries[0].model); if (shouldAddReduceExpression) { // add reducer to the second position // we only update the refid and the model to point to the reducer expression - state.queries[1].model.expression = SimpleConditionIdentifier.reducerId; + state.queries[1].model.expression = NEW_REDUCER_REF; + // insert in second position the reducer expression state.queries.splice(1, 0, { datasourceUid: ExpressionDatasourceUID, @@ -288,10 +275,10 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder type: ExpressionQueryType.reduce, reducer: ReducerID.last, conditions: [{ ...defaultCondition, query: { params: [] } }], - expression: SimpleConditionIdentifier.queryId, - refId: SimpleConditionIdentifier.reducerId, + expression: dataQuery?.refId, + refId: NEW_REDUCER_REF, }), - refId: SimpleConditionIdentifier.reducerId, + refId: NEW_REDUCER_REF, queryType: 'expression', }); } diff --git a/public/app/features/alerting/unified/components/rule-editor/util.test.ts b/public/app/features/alerting/unified/components/rule-editor/util.test.ts index 680367f38ec..13a4a6480b5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.test.ts @@ -2,6 +2,7 @@ import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWi import { ClassicCondition, ExpressionQuery } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { NEW_REDUCER_REF } from './query-and-alert-condition/reducer'; import { containsPathSeparator, findRenamedDataQueryReferences, @@ -163,10 +164,10 @@ describe('rule-editor', () => { it('should rewire threshold expressions', () => { const queries: AlertQuery[] = [dataSource, reduceExpression, thresholdExpression]; - const rewiredQueries = queriesWithUpdatedReferences(queries, 'B', 'REDUCER'); + const rewiredQueries = queriesWithUpdatedReferences(queries, 'B', NEW_REDUCER_REF); const queryModel = rewiredQueries[2].model as ExpressionQuery; - expect(queryModel.expression).toBe('REDUCER'); + expect(queryModel.expression).toBe(NEW_REDUCER_REF); }); it('should rewire multiple expressions', () => { diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index 8653c39f707..c5923c00c02 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -17,6 +17,7 @@ import { mockDataSource, mockPluginLinkExtension, mockPromAlertingRule, + mockRulerGrafanaRecordingRule, } from '../../mocks'; import { grafanaRulerRule } from '../../mocks/grafanaRulerApi'; import { grantPermissionsHelper } from '../../test/test-utils'; @@ -184,6 +185,30 @@ describe('RuleViewer', () => { } }); + it('shows paused state correctly for recording rules', async () => { + const recordingRule = getGrafanaRule({ + name: 'Test recording rule', + rulerRule: mockRulerGrafanaRecordingRule( + {}, + { + is_paused: true, + title: 'Test recording', + record: { + metric: 'test_recording', + from: 'A', + }, + } + ), + }); + + const recordingRuleIdentifier = ruleId.fromCombinedRule('grafana', recordingRule); + await renderRuleViewer(recordingRule, recordingRuleIdentifier, ActiveTab.Details); + + expect(await screen.findByText('Test recording rule')).toBeInTheDocument(); + expect(await screen.findByRole('status', { name: 'Alert evaluation currently paused' })).toBeInTheDocument(); + expect(screen.queryByText(/last evaluation duration/i)).not.toBeInTheDocument(); + }); + it('renders silencing form correctly and shows alert rule name', async () => { await renderRuleViewer(mockRule, mockRuleIdentifier); await openSilenceDrawer(); diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 82e46f5c574..6a25509e67c 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -73,7 +73,7 @@ const RuleViewer = () => { const isFederatedRule = isFederatedRuleGroup(rule.group); const isProvisioned = rulerRuleType.grafana.rule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance); - const isPaused = rulerRuleType.grafana.alertingRule(rulerRule) && isPausedRule(rulerRule); + const isPaused = rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule); const showError = hasError && !isPaused; const ruleOrigin = rulerRule ? getRulePluginOrigin(rulerRule) : getRulePluginOrigin(promRule); diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.test.tsx new file mode 100644 index 00000000000..ba354f056fd --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from 'test/test-utils'; + +import { mockCombinedRule } from '../../../mocks'; +import { alertingFactory } from '../../../mocks/server/db'; +import { setupDataSources } from '../../../testSetup/datasources'; + +import { Details } from './Details'; + +beforeAll(() => { + setupDataSources(); +}); + +describe('render details tab', () => { + it('should show paused rule', () => { + const rule = mockCombinedRule({ + rulerRule: alertingFactory.ruler.grafana.recordingRule.build({ + grafana_alert: { + is_paused: true, + }, + }), + }); + + render(
); + expect(screen.getByText(/Alert evaluation currently paused/i)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx index 59d4d2d30d1..7e87437d407 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx @@ -74,7 +74,7 @@ export const Details = ({ rule }: DetailsProps) => { const hasEvaluationDuration = Number.isFinite(evaluationDuration); const updated = rulerRuleType.grafana.rule(rule.rulerRule) ? rule.rulerRule.grafana_alert.updated : undefined; - const isPaused = rulerRuleType.grafana.alertingRule(rule.rulerRule) && isPausedRule(rule.rulerRule); + const isPaused = rulerRuleType.grafana.rule(rule.rulerRule) && isPausedRule(rule.rulerRule); const pausedIcon = ( diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index b5a0aeabc62..38c390f8e00 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -353,7 +353,7 @@ function useRuleStatus(rule: CombinedRule) { const isDeleting = Boolean(hasRuler && rulerRulesLoaded && promRule && !rulerRule); const isCreating = Boolean(hasRuler && rulerRulesLoaded && rulerRule && !promRule); - const isPaused = rulerRuleType.grafana.alertingRule(rulerRule) && isPausedRule(rulerRule); + const isPaused = rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule); return { isDeleting, isCreating, isPaused }; } diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts index 6350144a583..e4278cb9852 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts @@ -184,17 +184,21 @@ export function attachRulerRuleToCombinedRule(rule: CombinedRule, rulerGroup: Ru const combinedRulesFromRuler = rulerGroup.rules.map((rulerRule) => rulerRuleToCombinedRule(rulerRule, rule.namespace, rule.group) ); - const existingRulerRulesByName = combinedRulesFromRuler.reduce((acc, rule) => { - const sameNameRules = acc.get(rule.name); - if (sameNameRules) { - sameNameRules.push(rule); + const existingRulerRulesByUidOrName = combinedRulesFromRuler.reduce((acc, rule) => { + const sameNameOrUidRules = acc.get(getIdToCompare(rule)); + if (sameNameOrUidRules) { + sameNameOrUidRules.push(rule); } else { - acc.set(rule.name, [rule]); + acc.set(getIdToCompare(rule), [rule]); } return acc; }, new Map()); - const matchingRulerRule = getExistingRuleInGroup(rule.promRule, existingRulerRulesByName, rule.namespace.rulesSource); + const matchingRulerRule = getExistingRuleInGroup( + rule.promRule, + existingRulerRulesByUidOrName, + rule.namespace.rulesSource + ); if (matchingRulerRule) { rule.rulerRule = matchingRulerRule.rulerRule; rule.query = matchingRulerRule.query; @@ -283,15 +287,15 @@ export function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespac ...calculateGroupTotals(group), }; - const combinedRulesByName = new Map(); + const combinedRulesByUidOrName = new Map(); combinedGroup!.rules.forEach((r) => { // Prometheus rules do not have to be unique by name - const existingRule = combinedRulesByName.get(r.name); - existingRule ? existingRule.push(r) : combinedRulesByName.set(r.name, [r]); + const existingRule = combinedRulesByUidOrName.get(getIdToCompare(r)); + existingRule ? existingRule.push(r) : combinedRulesByUidOrName.set(getIdToCompare(r), [r]); }); (group.rules ?? []).forEach((rule) => { - const existingRule = getExistingRuleInGroup(rule, combinedRulesByName, namespace.rulesSource); + const existingRule = getExistingRuleInGroup(rule, combinedRulesByUidOrName, namespace.rulesSource); if (existingRule) { existingRule.promRule = rule; existingRule.instanceTotals = prometheusRuleType.alertingRule(rule) ? calculateRuleTotals(rule) : {}; @@ -405,6 +409,7 @@ function rulerRuleToCombinedRule( group, instanceTotals: {}, filteredInstanceTotals: {}, + uid: rulerRuleType.grafana.rule(rule) ? rule.grafana_alert.uid : undefined, } : rulerRuleType.dataSource.recordingRule(rule) ? { @@ -417,6 +422,7 @@ function rulerRuleToCombinedRule( group, instanceTotals: {}, filteredInstanceTotals: {}, + uid: rulerRuleType.grafana.rule(rule) ? rule.grafana_alert.uid : undefined, } : { name: rule.grafana_alert.title, @@ -428,30 +434,31 @@ function rulerRuleToCombinedRule( group, instanceTotals: {}, filteredInstanceTotals: {}, + uid: rulerRuleType.grafana.rule(rule) ? rule.grafana_alert.uid : undefined, }; } // find existing rule in group that matches the given prom rule function getExistingRuleInGroup( rule: Rule, - existingCombinedRulesMap: Map, + existingCombinedRulesMap: Map, // map by uid rulesSource: RulesSource ): CombinedRule | undefined { // Using Map of name-based rules is important performance optimization for the code below // Otherwise we would perform find method multiple times on (possibly) thousands of rules - const nameMatchingRules = existingCombinedRulesMap.get(rule.name); - if (!nameMatchingRules) { + const fieldToSearch = getIdToCompare(rule); + const uidOrNameMatchingRules = existingCombinedRulesMap.get(fieldToSearch); + if (!uidOrNameMatchingRules) { return undefined; } if (isGrafanaRulesSource(rulesSource)) { - // assume grafana groups have only the one rule. check name anyway because paranoid - return nameMatchingRules[0]; + return uidOrNameMatchingRules[0]; } // try finding a rule that matches name, labels, annotations and query - const strictlyMatchingRule = nameMatchingRules.find( + const strictlyMatchingRule = uidOrNameMatchingRules.find( (combinedRule) => !combinedRule.promRule && isCombinedRuleEqualToPromRule(combinedRule, rule, true) ); if (strictlyMatchingRule) { @@ -460,7 +467,7 @@ function getExistingRuleInGroup( // if that fails, try finding a rule that only matches name, labels and annotations. // loki & prom can sometimes modify the query so it doesnt match, eg `2 > 1` becomes `1` - const looselyMatchingRule = nameMatchingRules.find( + const looselyMatchingRule = uidOrNameMatchingRules.find( (combinedRule) => !combinedRule.promRule && isCombinedRuleEqualToPromRule(combinedRule, rule, false) ); if (looselyMatchingRule) { @@ -591,3 +598,14 @@ export function useCombinedRules( result: rules, }; } +/** + * Returns the ID of the rule to compare. + * If the rule has a UID, it will be used, otherwise the rule name will be used. + * cloud rules don't have a UID, so the name will be used. + */ +function getIdToCompare(rule: CombinedRule | Rule): string { + if (Boolean(rule.uid)) { + return rule.uid ?? ''; + } + return rule.name; +} diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 8e2701e0e04..4e0cd678109 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -56,7 +56,6 @@ import { import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types'; -import { SimpleConditionIdentifier } from './components/rule-editor/query-and-alert-condition/SimpleCondition'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers'; @@ -772,30 +771,33 @@ export function mockDashboardDto( }; } -export const dataQuery: AlertQuery = { - refId: SimpleConditionIdentifier.queryId, +export const mockDataQuery = (partial: Partial = {}): AlertQuery => ({ + refId: partial?.refId ?? 'A', datasourceUid: 'abc123', queryType: '', - model: { refId: SimpleConditionIdentifier.queryId }, -}; + model: { refId: 'A', ...partial }, +}); -export const reduceExpression: AlertQuery = { - refId: SimpleConditionIdentifier.reducerId, +export const mockReduceExpression = (partial: Partial = {}): AlertQuery => ({ + refId: 'B', queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.reduce, - refId: SimpleConditionIdentifier.reducerId, + refId: 'B', settings: { mode: ReducerMode.Strict }, reducer: ReducerID.last, + ...partial, }, -}; -export const thresholdExpression: AlertQuery = { - refId: SimpleConditionIdentifier.thresholdId, +}); + +export const mockThresholdExpression = (partial: Partial = {}): AlertQuery => ({ + refId: 'C', queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.threshold, - refId: SimpleConditionIdentifier.thresholdId, + refId: 'C', + ...partial, }, -}; +}); diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index be2e261ba2f..14dac82f6cb 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -5,11 +5,13 @@ import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { FolderDTO } from 'app/types'; import { + GrafanaRecordingRuleDefinition, PromAlertingRuleDTO, PromAlertingRuleState, PromRuleGroupDTO, PromRuleType, RulerAlertingRuleDTO, + RulerGrafanaRuleDTO, RulerRecordingRuleDTO, RulerRuleGroupDTO, } from 'app/types/unified-alerting-dto'; @@ -114,6 +116,25 @@ const grafanaFolderFactory = Factory.define(({ sequence }) => ({ updatedBy: '', })); +const grafanaRecordingRule = Factory.define>(({ sequence }) => ({ + grafana_alert: { + id: String(sequence), + uid: uniqueId(), + title: `Recording rule ${sequence}`, + namespace_uid: 'test-namespace', + rule_group: 'test-group', + condition: 'A', + data: [], + record: { + from: 'vector(1)', + metric: `recording_rule_${sequence}`, + }, + }, + for: '5m', + labels: { 'label-key-1': 'label-value-1' }, + annotations: {}, // @TODO recording rules don't have annotations, we need to fix this type definition +})); + export const alertingFactory = { folder: grafanaFolderFactory, prometheus: { @@ -124,6 +145,9 @@ export const alertingFactory = { group: rulerRuleGroupFactory, alertingRule: rulerAlertingRuleFactory, recordingRule: rulerRecordingRuleFactory, + grafana: { + recordingRule: grafanaRecordingRule, + }, }, dataSource: dataSourceFactory, }; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/provisioning.ts b/public/app/features/alerting/unified/mocks/server/handlers/provisioning.ts index 44714487295..6d92f279f5c 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/provisioning.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/provisioning.ts @@ -17,5 +17,11 @@ const exportMuteTimingsHandler = () => http.get('/api/v1/provisioning/mute-timin const exportSpecificMuteTimingsHandler = () => http.get('/api/v1/provisioning/mute-timings/:name/export', getProvisioningHelper); -const handlers = [exportMuteTimingsHandler(), exportSpecificMuteTimingsHandler()]; +// Used by folder picker to load provisioning settings +const provisioningSettingsHandler = () => + http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => + HttpResponse.json({ items: [] }) + ); + +const handlers = [exportMuteTimingsHandler(), exportSpecificMuteTimingsHandler(), provisioningSettingsHandler()]; export default handlers; diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts index 800b95a6193..9dd60a76b8d 100644 --- a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; -import { mockAlertQuery, mockDataSource, reduceExpression, thresholdExpression } from '../mocks'; +import { mockAlertQuery, mockDataSource, mockReduceExpression, mockThresholdExpression } from '../mocks'; import { testWithFeatureToggles } from '../test/test-utils'; import { RuleFormType } from '../types/rule-form'; import { Annotation } from '../utils/constants'; @@ -73,7 +73,11 @@ describe('formValuesFromQueryParams', () => { it('should enable simplified query editor if queries are transformable to simple condition', () => { const result = formValuesFromQueryParams( JSON.stringify({ - queries: [mockAlertQuery(), reduceExpression, thresholdExpression], + queries: [ + mockAlertQuery(), + mockReduceExpression({ expression: 'A' }), + mockThresholdExpression({ expression: 'B' }), + ], }), RuleFormType.grafana ); @@ -85,7 +89,7 @@ describe('formValuesFromQueryParams', () => { it('should disable simplified query editor if queries are not transformable to simple condition', () => { const result = formValuesFromQueryParams( JSON.stringify({ - queries: [mockAlertQuery(), mockAlertQuery(), thresholdExpression], + queries: [mockAlertQuery(), mockAlertQuery(), mockThresholdExpression({ expression: 'B' })], }), RuleFormType.grafana ); diff --git a/public/app/features/alerting/unified/rule-editor/formProcessing.ts b/public/app/features/alerting/unified/rule-editor/formProcessing.ts index c9fa1951417..d9f79892ce5 100644 --- a/public/app/features/alerting/unified/rule-editor/formProcessing.ts +++ b/public/app/features/alerting/unified/rule-editor/formProcessing.ts @@ -1,14 +1,15 @@ -import { omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { config } from '@grafana/runtime'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types'; +import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; +import { isStrictReducer } from 'app/features/expressions/utils/expressionTypes'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { SimpleConditionIdentifier } from '../components/rule-editor/query-and-alert-condition/SimpleCondition'; import { KVObject, RuleFormValues } from '../types/rule-form'; import { defaultAnnotations } from '../utils/constants'; import { DataSourceType } from '../utils/datasource'; +import { getInstantFromDataQuery } from '../utils/rule-form'; export function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { const isQuerySwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; @@ -64,47 +65,63 @@ export function setInstantOrRange(values: RuleFormValues): RuleFormValues { }; } +/** + * A alert rule is "transformable" to a simple condition editor if + * 1. we have a single data query + * 2. we have _either_ + * 2.1 a reduce expression (pointing to the data query) _and_ a threshold expression pointing to the reducer + * 2.2 a threshold expression pointing to a (instant) data query + * ⚠️ do not assert on refIds or indexes of the queries + */ export function areQueriesTransformableToSimpleCondition( - dataQueries: Array>, + dataQueries: Array>, expressionQueries: Array> ) { + // 1. check if we only have a _single_ data query if (dataQueries.length !== 1) { return false; } - const singleReduceExpressionInInstantQuery = - 'instant' in dataQueries[0].model && dataQueries[0].model.instant && expressionQueries.length === 1; - if (expressionQueries.length !== 2 && !singleReduceExpressionInInstantQuery) { + // short-circuit when we have more than 2 expressions, we don't know what to do with that + if (expressionQueries.length > 2) { return false; } - const query = dataQueries[0]; + const dataQuery = dataQueries.at(0); - if (query.refId !== SimpleConditionIdentifier.queryId) { - return false; + // find the reduce or threshold expressions + const reduceExpression = expressionQueries.find((query) => query.model.type === ExpressionQueryType.reduce); + const thresholdExpression = expressionQueries.find((query) => query.model.type === ExpressionQueryType.threshold); + + // reducer should be set to "strict" mode + const reducerIsStrict = reduceExpression ? isStrictReducer(reduceExpression.model) : false; + // threshold expression shouldn't have an unload evaluator (custom recovery threshold) + const thresholdExpressionIsClean = + thresholdExpression?.model.conditions?.every((condition) => { + return isEmpty(condition.unloadEvaluator); + }) ?? true; + + const validReducerExpression = reduceExpression && reducerIsStrict; + const validThresholdExpression = thresholdExpression && thresholdExpressionIsClean; + + const thresholdPointingToReducer = thresholdExpression?.model.expression === reduceExpression?.refId; + const reducerPointingToDataQuery = reduceExpression?.model.expression === dataQuery?.refId; + + // 2.1 check for a reduce + threshold expression and their targets + if (validReducerExpression && reducerPointingToDataQuery && validThresholdExpression && thresholdPointingToReducer) { + return true; } - const reduceExpressionIndex = expressionQueries.findIndex( - (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId - ); - const reduceExpression = expressionQueries.at(reduceExpressionIndex); - const reduceOk = - reduceExpression && - reduceExpressionIndex === 0 && - (reduceExpression.model.settings?.mode === ReducerMode.Strict || - reduceExpression.model.settings?.mode === undefined); + // 2.2 check for a single threshold expression pointing to an "instant" data query + const isInstantDataQuery = dataQuery ? getInstantFromDataQuery(dataQuery) : false; + const hasSingleThresholdExpression = expressionQueries.length === 1 && thresholdExpression; + const thresholdPointingToDataQuery = thresholdExpression?.model.expression === dataQuery?.refId; - const thresholdExpressionIndex = expressionQueries.findIndex( - (query) => - query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId - ); - const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); - const conditions = thresholdExpression?.model.conditions ?? []; - const thresholdIndexOk = singleReduceExpressionInInstantQuery - ? thresholdExpressionIndex === 0 - : thresholdExpressionIndex === 1; - const thresholdOk = thresholdExpression && thresholdIndexOk && conditions[0]?.unloadEvaluator === undefined; - return (Boolean(reduceOk) || Boolean(singleReduceExpressionInInstantQuery)) && Boolean(thresholdOk); + if (isInstantDataQuery && hasSingleThresholdExpression && validThresholdExpression && thresholdPointingToDataQuery) { + return true; + } + + return false; } export function isExpressionQueryInAlert( diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index e8939c0c336..5c13f97c954 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,10 +1,18 @@ import { PromQuery } from '@grafana/prometheus'; -import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; +import { + AlertDataQuery, + AlertQuery, + GrafanaAlertStateDecision, + GrafanaRuleDefinition, + RulerAlertingRuleDTO, +} from 'app/types/unified-alerting-dto'; +import { mockDataSource } from '../mocks'; import { getDefaultFormValues } from '../rule-editor/formDefaults'; +import { setupDataSources } from '../testSetup/datasources'; import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../types/rule-form'; -import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; +import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { alertingRulerRuleToRuleForm, cleanAnnotations, @@ -12,6 +20,7 @@ import { formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, + getInstantFromDataQuery, getNotificationSettingsForDTO, } from './rule-form'; @@ -254,3 +263,71 @@ describe('cleanLabels', () => { expect(output).toStrictEqual([{ key: 'key', value: '' }]); }); }); + +describe('getInstantFromDataQuery', () => { + const query: AlertQuery = { + refId: 'Q', + datasourceUid: 'abc123', + queryType: '', + relativeTimeRange: { + from: 600, + to: 0, + }, + model: { + refId: 'Q', + }, + }; + + it('should return undefined if datasource UID is undefined', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Prometheus, name: 'Mimir-cloud', uid: 'mimir-1' })); + const result = getInstantFromDataQuery({ ...query }); + expect(result).toBeUndefined(); + }); + + it('should return undefined if datasource type is not Prometheus or Loki', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Alertmanager, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ ...query, datasourceUid: 'aa' }); + expect(result).toBeUndefined(); + }); + + it('should return true if datasource is Prometheus and instant is not defined', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Prometheus, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ ...query, datasourceUid: 'aa' }); + + expect(result).toBe(true); + }); + + it('should return the value of instant if datasource is Prometheus and instant is defined', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Prometheus, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ ...query, datasourceUid: 'aa', model: { refId: 'f', instant: false } }); + expect(result).toBe(false); + }); + + it('should return true if datasource is Loki and queryType is not defined', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Loki, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ ...query, datasourceUid: 'aa' }); + expect(result).toBe(true); + }); + + it('should return true if datasource is Loki and queryType is instant', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Loki, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ + ...query, + datasourceUid: 'aa', + model: { refId: 'f', queryType: 'instant' }, + }); + + expect(result).toBe(true); + }); + + it('should return false if datasource is Loki and queryType is not instant', () => { + setupDataSources(mockDataSource({ type: DataSourceType.Loki, name: 'aa', uid: 'aa-1' })); + const result = getInstantFromDataQuery({ + ...query, + datasourceUid: 'aa', + model: { refId: 'f', queryType: 'range' }, + }); + + expect(result).toBe(false); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 194b7bec552..7845c5e228f 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -803,19 +803,22 @@ export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQue return 'expr' in model; } -export function getInstantFromDataQuery(model: AlertDataQuery, type: string): boolean | undefined { - // if the datasource is not prometheus or loki, instant is defined in the model or defaults to undefined - if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) { - if ('instant' in model) { - return model.instant; - } else { - if ('queryType' in model) { - return model.queryType === 'instant'; - } else { - return undefined; - } - } +export function getInstantFromDataQuery(query: AlertQuery): boolean | undefined { + const dataSourceUID = query.datasourceUid ?? query.model.datasource?.uid; + if (!dataSourceUID) { + return undefined; } + + // find the datasource type from the UID + const type = getDataSourceSrv().getInstanceSettings(dataSourceUID)?.type; + + // if the datasource is not prometheus or loki, return "undefined" + if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) { + return undefined; + } + + const { model } = query; + // if the datasource is prometheus or loki, instant is defined in the model, or defaults to true const isInstantForPrometheus = 'instant' in model && model.instant !== undefined ? model.instant : true; const isInstantForLoki = 'queryType' in model && model.queryType !== undefined ? model.queryType === 'instant' : true; diff --git a/public/app/features/api-keys/ApiKeysActionBar.tsx b/public/app/features/api-keys/ApiKeysActionBar.tsx index e5edff671ac..ffae1b7cbc7 100644 --- a/public/app/features/api-keys/ApiKeysActionBar.tsx +++ b/public/app/features/api-keys/ApiKeysActionBar.tsx @@ -1,4 +1,5 @@ import { FilterInput, InlineField } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; interface Props { searchQuery: string; @@ -10,7 +11,11 @@ export const ApiKeysActionBar = ({ searchQuery, disabled, onSearchChange }: Prop return (
- +
); diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 1f3e769e86b..28cc53bca23 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -6,7 +6,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { InlineField, InlineSwitch, Modal, Button, EmptyState } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { t } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { AccessControlAction, ApiKey, ApikeyMigrationResult, StoreState } from 'app/types'; @@ -127,7 +127,10 @@ export class ApiKeysPageUnconnected extends PureComponent { onSearchChange={this.onSearchQueryChange} /> ) : null} - + {apiKeys.length > 0 ? ( @@ -175,10 +178,17 @@ const styles: { [key: string]: React.CSSProperties } = { export const MigrationSummary: React.FC = ({ visible, data, onDismiss }) => { return ( - + {data.failedApikeyIDs.length === 0 && (
-

Migration Successful!

+

+ Migration Successful! +

Total: {data.total} @@ -220,7 +230,7 @@ export const MigrationSummary: React.FC = ({ visible, dat )} diff --git a/public/app/features/api-keys/ApiKeysTable.tsx b/public/app/features/api-keys/ApiKeysTable.tsx index f695fe5d4e1..983903d9c15 100644 --- a/public/app/features/api-keys/ApiKeysTable.tsx +++ b/public/app/features/api-keys/ApiKeysTable.tsx @@ -3,6 +3,7 @@ import { css } from '@emotion/css'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; import { Button, DeleteButton, Icon, Stack, Tooltip, useTheme2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; +import { Trans, t } from 'app/core/internationalization'; import { AccessControlAction } from 'app/types'; import { ApiKey } from '../../types'; @@ -22,10 +23,18 @@ export const ApiKeysTable = ({ apiKeys, timeZone, onDelete, onMigrate }: Props)

- - - - + + + + @@ -51,10 +60,12 @@ export const ApiKeysTable = ({ apiKeys, timeZone, onDelete, onMigrate }: Props)
NameRoleExpiresLast used at + Name + + Role + + Expires + + Last used at +
onDelete(key)} disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.ActionAPIKeysDelete, key)} diff --git a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx index 87ff5e72ed4..aa9f20fa292 100644 --- a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx +++ b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, Button, ConfirmModal, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; interface Props { onMigrate: () => void; @@ -21,15 +22,29 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, apikeysCount, disabled target="_blank" rel="noopener noreferrer" > - Find out more about the migration here. + + Find out more about the migration here. + ); - const migrationBoxDesc = Migrating all API keys will hide the API keys tab.; + const migrationBoxDesc = ( + + + Migrating all API keys will hide the API keys tab. + + + ); return ( <> {apikeysCount > 0 && ( - +
API keys are deprecated and will be removed from Grafana on Jan 31, 2025. Each API key will be migrated into a service account with a token and will continue to work as they were. We encourage you to migrate your API @@ -37,7 +52,9 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, apikeysCount, disabled
- +
No API keys were found. If you reload the browser, this page will not be available anymore.
diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index cb53c57aa65..2af5503674f 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -1,6 +1,7 @@ import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs'; -import { BackendSrvRequest, config, getBackendSrv } from '@grafana/runtime'; +import { isLiveChannelMessageEvent, LiveChannelScope } from '@grafana/data'; +import { config, getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getAPINamespace } from '../../api/utils'; @@ -19,20 +20,16 @@ import { K8sAPIGroupList, AnnoKeySavedFromUI, ResourceEvent, + GroupVersionResource, } from './types'; -export interface GroupVersionResource { - group: string; - version: string; - resource: string; -} - export class ScopedResourceClient implements ResourceClient { readonly url: string; + readonly gvr: GroupVersionResource; constructor(gvr: GroupVersionResource, namespaced = true) { const ns = namespaced ? `namespaces/${getAPINamespace()}/` : ''; - + this.gvr = gvr; this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`; } @@ -40,26 +37,40 @@ export class ScopedResourceClient implements return getBackendSrv().get>(`${this.url}/${name}`); } - public watch( - params?: WatchOptions, - config?: Pick - ): Observable> { - const decoder = new TextDecoder(); - const { name, ...rest } = params ?? {}; // name needs to be added to fieldSelector + public watch(params?: WatchOptions): Observable> { const requestParams = { - ...rest, watch: true, labelSelector: this.parseListOptionsSelector(params?.labelSelector), fieldSelector: this.parseListOptionsSelector(params?.fieldSelector), }; - if (name) { + if (params?.name) { requestParams.fieldSelector = `metadata.name=${name}`; } + + // For now, watch over live only supports provisioning + if (this.gvr.group === 'provisioning.grafana.app') { + let query = ''; + if (requestParams.fieldSelector?.startsWith('metadata.name=')) { + query = requestParams.fieldSelector.substring('metadata.name'.length); + } + return getGrafanaLiveSrv() + .getStream>({ + scope: LiveChannelScope.Watch, + namespace: this.gvr.group, + path: `${this.gvr.version}/${this.gvr.resource}${query}/${config.bootData.user.uid}`, + }) + .pipe( + filter((event) => isLiveChannelMessageEvent(event)), + map((event) => event.message) + ); + } + + const decoder = new TextDecoder(); return getBackendSrv() .chunked({ url: this.url, params: requestParams, - ...config, + method: 'GET', }) .pipe( filter((response) => response.ok && response.data instanceof Uint8Array), @@ -73,7 +84,7 @@ export class ScopedResourceClient implements try { return JSON.parse(line); } catch (e) { - console.warn('Invalid JSON in watch stream:', e); + console.warn('Invalid JSON in watch stream:', e, line); return null; } }), diff --git a/public/app/features/apiserver/discovery.test.ts b/public/app/features/apiserver/discovery.test.ts new file mode 100644 index 00000000000..5b784197eea --- /dev/null +++ b/public/app/features/apiserver/discovery.test.ts @@ -0,0 +1,12 @@ +import { discoveryResources } from './discovery'; + +const discoverySnapshot = require('./snapshots/discovery-snapshot.json'); + +describe('simple typescript tests', () => { + it('simple', async () => { + const watchable = discoveryResources(discoverySnapshot) + .filter((v) => v.verbs.includes('watch')) + .map((v) => v.resource); + expect(watchable).toEqual(['user-storage', 'dashboards', 'dashboards', 'dashboards']); + }); +}); diff --git a/public/app/features/apiserver/discovery.ts b/public/app/features/apiserver/discovery.ts new file mode 100644 index 00000000000..ece21681d48 --- /dev/null +++ b/public/app/features/apiserver/discovery.ts @@ -0,0 +1,84 @@ +import { lastValueFrom, map } from 'rxjs'; + +import { FetchResponse, getBackendSrv } from '@grafana/runtime'; + +import { GroupVersionKind, ListMeta } from './types'; + +export type GroupDiscoveryResource = { + resource: string; + responseKind: GroupVersionKind; + scope: 'Namespaced' | 'Cluster'; + singularResource: string; + verbs: string[]; + subresources?: GroupDiscoverySubresource[]; +}; + +export type GroupDiscoverySubresource = { + subresource: string; + responseKind: GroupVersionKind; + verbs: string[]; +}; + +export type GroupDiscoveryVersion = { + version: string; + freshness: 'Current' | string; + resources: GroupDiscoveryResource[]; +}; + +export type GroupDiscoveryItem = { + metadata: { + name: string; + }; + versions: GroupDiscoveryVersion[]; +}; + +export type APIGroupDiscoveryList = { + metadata: ListMeta; + items: GroupDiscoveryItem[]; +}; + +export async function getAPIGroupDiscoveryList(): Promise { + return await lastValueFrom( + getBackendSrv() + .fetch({ + method: 'GET', + url: '/apis', + headers: { + Accept: + 'application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json', + }, + }) + .pipe( + map((response: FetchResponse) => { + // Fill in the group+version before returning + for (let api of response.data.items) { + for (let version of api.versions) { + for (let resource of version.resources) { + resource.responseKind.group = api.metadata.name; + resource.responseKind.version = version.version; + if (resource.subresources) { + for (let sub of resource.subresources) { + sub.responseKind.group = api.metadata.name; + sub.responseKind.version = version.version; + } + } + } + } + } + return response.data; + }) + ) + ); +} + +export function discoveryResources(apis: APIGroupDiscoveryList): GroupDiscoveryResource[] { + const resources: GroupDiscoveryResource[] = []; + for (let api of apis.items) { + for (let version of api.versions) { + for (let resource of version.resources) { + resources.push(resource); + } + } + } + return resources; +} diff --git a/public/app/features/apiserver/snapshots/discovery-snapshot.json b/public/app/features/apiserver/snapshots/discovery-snapshot.json new file mode 100644 index 00000000000..36fc7f05aa3 --- /dev/null +++ b/public/app/features/apiserver/snapshots/discovery-snapshot.json @@ -0,0 +1,295 @@ +{ + "kind": "APIGroupDiscoveryList", + "apiVersion": "apidiscovery.k8s.io/v2", + "metadata": {}, + "items": [ + { + "metadata": { "name": "userstorage.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "user-storage", + "responseKind": { "group": "", "version": "", "kind": "UserStorage" }, + "scope": "Namespaced", + "singularResource": "user-storage", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "notifications.alerting.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "receivers", + "responseKind": { "group": "", "version": "", "kind": "Receiver" }, + "scope": "Namespaced", + "singularResource": "receiver", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"] + }, + { + "resource": "routingtrees", + "responseKind": { "group": "", "version": "", "kind": "RoutingTree" }, + "scope": "Namespaced", + "singularResource": "routingtree", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"] + }, + { + "resource": "templategroups", + "responseKind": { "group": "", "version": "", "kind": "TemplateGroup" }, + "scope": "Namespaced", + "singularResource": "templategroup", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"] + }, + { + "resource": "timeintervals", + "responseKind": { "group": "", "version": "", "kind": "TimeInterval" }, + "scope": "Namespaced", + "singularResource": "timeinterval", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "iam.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "serviceaccounts", + "responseKind": { "group": "", "version": "", "kind": "ServiceAccount" }, + "scope": "Namespaced", + "singularResource": "serviceaccount", + "verbs": ["get", "list"], + "subresources": [ + { + "subresource": "tokens", + "responseKind": { "group": "", "version": "", "kind": "UserTeamList" }, + "verbs": ["get"] + } + ] + }, + { + "resource": "ssosettings", + "responseKind": { "group": "", "version": "", "kind": "SSOSetting" }, + "scope": "Namespaced", + "singularResource": "ssosetting", + "verbs": ["delete", "get", "list", "patch", "update"] + }, + { + "resource": "teambindings", + "responseKind": { "group": "", "version": "", "kind": "TeamBinding" }, + "scope": "Namespaced", + "singularResource": "teambinding", + "verbs": ["get", "list"] + }, + { + "resource": "teams", + "responseKind": { "group": "", "version": "", "kind": "Team" }, + "scope": "Namespaced", + "singularResource": "team", + "verbs": ["get", "list"], + "subresources": [ + { + "subresource": "members", + "responseKind": { "group": "", "version": "", "kind": "TeamMemberList" }, + "verbs": ["get"] + } + ] + }, + { + "resource": "users", + "responseKind": { "group": "", "version": "", "kind": "User" }, + "scope": "Namespaced", + "singularResource": "user", + "verbs": ["get", "list"], + "subresources": [ + { + "subresource": "teams", + "responseKind": { "group": "", "version": "", "kind": "UserTeamList" }, + "verbs": ["get"] + } + ] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "folder.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "folders", + "responseKind": { "group": "", "version": "", "kind": "Folder" }, + "scope": "Namespaced", + "singularResource": "folder", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"], + "subresources": [ + { + "subresource": "access", + "responseKind": { "group": "", "version": "", "kind": "FolderAccessInfo" }, + "verbs": ["get"] + }, + { + "subresource": "counts", + "responseKind": { "group": "", "version": "", "kind": "DescendantCounts" }, + "verbs": ["get"] + }, + { + "subresource": "parents", + "responseKind": { "group": "", "version": "", "kind": "FolderInfoList" }, + "verbs": ["get"] + } + ] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "featuretoggle.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "features", + "responseKind": { "group": "", "version": "", "kind": "Feature" }, + "scope": "Cluster", + "singularResource": "feature", + "verbs": ["get", "list"] + }, + { + "resource": "featuretoggles", + "responseKind": { "group": "", "version": "", "kind": "FeatureToggles" }, + "scope": "Namespaced", + "singularResource": "featuretoggle", + "verbs": ["get", "list"] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "dashboard.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "dashboards", + "responseKind": { "group": "", "version": "", "kind": "Dashboard" }, + "scope": "Namespaced", + "singularResource": "dashboard", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"], + "subresources": [ + { + "subresource": "dto", + "responseKind": { "group": "", "version": "", "kind": "DashboardWithAccessInfo" }, + "verbs": ["get"] + } + ] + }, + { + "resource": "librarypanels", + "responseKind": { "group": "", "version": "", "kind": "LibraryPanel" }, + "scope": "Namespaced", + "singularResource": "librarypanel", + "verbs": ["get", "list"] + } + ], + "freshness": "Current" + }, + { + "version": "v1alpha1", + "resources": [ + { + "resource": "dashboards", + "responseKind": { "group": "", "version": "", "kind": "Dashboard" }, + "scope": "Namespaced", + "singularResource": "dashboard", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"], + "subresources": [ + { + "subresource": "dto", + "responseKind": { "group": "", "version": "", "kind": "DashboardWithAccessInfo" }, + "verbs": ["get"] + } + ] + }, + { + "resource": "librarypanels", + "responseKind": { "group": "", "version": "", "kind": "LibraryPanel" }, + "scope": "Namespaced", + "singularResource": "librarypanel", + "verbs": ["get", "list"] + } + ], + "freshness": "Current" + }, + { + "version": "v2alpha1", + "resources": [ + { + "resource": "dashboards", + "responseKind": { "group": "", "version": "", "kind": "Dashboard" }, + "scope": "Namespaced", + "singularResource": "dashboard", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"], + "subresources": [ + { + "subresource": "dto", + "responseKind": { "group": "", "version": "", "kind": "DashboardWithAccessInfo" }, + "verbs": ["get"] + } + ] + }, + { + "resource": "librarypanels", + "responseKind": { "group": "", "version": "", "kind": "LibraryPanel" }, + "scope": "Namespaced", + "singularResource": "librarypanel", + "verbs": ["get", "list"] + } + ], + "freshness": "Current" + } + ] + }, + { + "metadata": { "name": "playlist.grafana.app", "creationTimestamp": null }, + "versions": [ + { + "version": "v0alpha1", + "resources": [ + { + "resource": "playlists", + "responseKind": { "group": "", "version": "", "kind": "Playlist" }, + "scope": "Namespaced", + "singularResource": "playlist", + "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update"] + } + ], + "freshness": "Current" + } + ] + } + ] +} diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 06199f6d677..631f56f505c 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -111,6 +111,18 @@ type GrafanaLabels = { [DeprecatedInternalId]?: string; }; +export interface GroupVersionResource { + group: string; + version: string; + resource: string; +} + +export interface GroupVersionKind { + group: string; + version: string; + kind: string; +} + export interface Resource extends TypeMeta { metadata: ObjectMeta; spec: T; diff --git a/public/app/features/auth-config/AuthDrawer.tsx b/public/app/features/auth-config/AuthDrawer.tsx index a9530e92d9f..b98e59855dd 100644 --- a/public/app/features/auth-config/AuthDrawer.tsx +++ b/public/app/features/auth-config/AuthDrawer.tsx @@ -5,6 +5,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, Drawer, Text, TextLink, Switch, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { t, Trans } from 'app/core/internationalization'; import { StoreState } from 'app/types'; import { loadSettings, saveSettings } from './state/actions'; @@ -84,10 +85,21 @@ export const AuthDrawerUnconnected = ({ const styles = useStyles2(getStyles); return ( - +
- Advanced Auth - Enable insecure email lookup + + Advanced Auth + + + + Enable insecure email lookup + + Allow users to use the same email address to log into Grafana with different identity providers. @@ -100,7 +112,7 @@ export const AuthDrawerUnconnected = ({ onClick={resetButtonOnClick} tooltip="This action will disregard any saved changes and load the configuration from the configuration file." > - Reset + Reset ); diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index 717906f0f3a..46909ed9397 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -6,6 +6,7 @@ import { reportInteraction } from '@grafana/runtime'; import { Grid, TextLink, ToolbarButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { config } from 'app/core/config'; +import { Trans } from 'app/core/internationalization'; import { StoreState } from 'app/types'; import AuthDrawer from './AuthDrawer'; @@ -95,7 +96,7 @@ export const AuthConfigPageUnconnected = ({ actions={ config.buildInfo.edition !== GrafanaEdition.OpenSource && ( setShowDrawer(true)}> - Auth settings + Auth settings ) } diff --git a/public/app/features/auth-config/ProviderConfigForm.tsx b/public/app/features/auth-config/ProviderConfigForm.tsx index 0fb3567a16b..cc6372500b8 100644 --- a/public/app/features/auth-config/ProviderConfigForm.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.tsx @@ -16,6 +16,7 @@ import { Stack, Switch, } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt'; import { Page } from '../../core/components/Page/Page'; @@ -54,7 +55,10 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf const additionalActionsMenu = ( { setResetConfig(true); @@ -163,7 +167,7 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf reset(); }} /> -
); diff --git a/public/app/features/browse-dashboards/components/CheckboxCell.tsx b/public/app/features/browse-dashboards/components/CheckboxCell.tsx index 26325c23f49..96a36b8e9bb 100644 --- a/public/app/features/browse-dashboards/components/CheckboxCell.tsx +++ b/public/app/features/browse-dashboards/components/CheckboxCell.tsx @@ -5,6 +5,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { Checkbox, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import { ManagerKind } from '../../apiserver/types'; import { DashboardsTreeCellProps, SelectionState } from '../types'; import { isSharedWithMe } from './utils'; @@ -41,6 +42,7 @@ export default function CheckboxCell({ value={state === SelectionState.Selected} indeterminate={state === SelectionState.Mixed} onChange={(ev) => onItemSelectionChange?.(item, ev.currentTarget.checked)} + disabled={item.managedBy === ManagerKind.Repo} /> ); } diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index ed06d0b14a2..600d0038224 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -5,6 +5,7 @@ import { locationUtil } from '@grafana/data'; import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { getImportPhrase, getNewDashboardPhrase, @@ -13,9 +14,11 @@ import { } from 'app/features/search/tempI18nPhrases'; import { FolderDTO } from 'app/types'; +import { ManagerKind } from '../../apiserver/types'; import { useNewFolderMutation } from '../api/browseDashboardsAPI'; import { NewFolderForm } from './NewFolderForm'; +import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; interface Props { parentFolder?: FolderDTO; @@ -29,6 +32,7 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC const [newFolder] = useNewFolderMutation(); const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false); const notifyApp = useAppNotification(); + const isProvisionedInstance = useIsProvisionedInstance(); const onCreateFolder = async (folderName: string) => { try { @@ -102,7 +106,15 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC onClose={() => setShowNewFolderDrawer(false)} size="sm" > - setShowNewFolderDrawer(false)} /> + {parentFolder?.managedBy === ManagerKind.Repo || isProvisionedInstance ? ( + setShowNewFolderDrawer(false)} + onCancel={() => setShowNewFolderDrawer(false)} + parentFolder={parentFolder} + /> + ) : ( + setShowNewFolderDrawer(false)} /> + )}
)} diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index 1c4f3984d86..b47bd044d5f 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -5,9 +5,11 @@ import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Permissions } from 'app/core/components/AccessControl'; import { appEvents } from 'app/core/core'; import { t, Trans } from 'app/core/internationalization'; +import { ProvisionedResourceDeleteModal } from 'app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal'; import { FolderDTO } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { ManagerKind } from '../../apiserver/types'; import { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI'; import { getFolderPermissions } from '../permissions'; @@ -24,8 +26,9 @@ export function FolderActionsButton({ folder }: Props) { const [moveFolder] = useMoveFolderMutation(); const [deleteFolder] = useDeleteFolderMutation(); const { canEditFolders, canDeleteFolders, canViewPermissions, canSetPermissions } = getFolderPermissions(folder); - // Can only move folders when nestedFolders is enabled - const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders; + const isProvisionedFolder = folder.managedBy === ManagerKind.Repo; + // Can only move folders when nestedFolders is enabled and the folder is not provisioned + const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders && !isProvisionedFolder; const onMove = async (destinationUID: string) => { await moveFolder({ folder, destinationUID }); @@ -86,6 +89,17 @@ export function FolderActionsButton({ folder }: Props) { ); }; + const showDeleteProvisionedModal = () => { + appEvents.publish( + new ShowModalReactEvent({ + component: ProvisionedResourceDeleteModal, + props: { + resource: folder, + }, + }) + ); + }; + const managePermissionsLabel = t('browse-dashboards.folder-actions-button.manage-permissions', 'Manage permissions'); const moveLabel = t('browse-dashboards.folder-actions-button.move', 'Move'); const deleteLabel = t('browse-dashboards.folder-actions-button.delete', 'Delete'); @@ -94,7 +108,13 @@ export function FolderActionsButton({ folder }: Props) { {canViewPermissions && setShowPermissionsDrawer(true)} label={managePermissionsLabel} />} {canMoveFolder && } - {canDeleteFolders && } + {canDeleteFolders && ( + + )} ); diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 6316f767cf2..56050295269 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -5,10 +5,11 @@ import { GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui'; import { getSvgSize } from '@grafana/ui/internal'; -import { t } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getIconForItem } from 'app/features/search/service/utils'; import { Indent } from '../../../core/components/Indent/Indent'; +import { FolderRepo } from '../../../core/components/NestedFolderPicker/FolderRepo'; import { useChildrenByParentUIDState } from '../state'; import { DashboardsTreeCellProps } from '../types'; @@ -42,7 +43,7 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam {item.uiKind === 'empty-folder' ? ( - No items + No items ) : ( @@ -102,6 +103,8 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam item.title )} + +
); diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx new file mode 100644 index 00000000000..f289e2f1fbb --- /dev/null +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx @@ -0,0 +1,450 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { AppEvents } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { useGetFolderQuery } from 'app/api/clients/folder'; +import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning'; +import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; +import { usePullRequestParam, useRepositoryList } from 'app/features/provisioning/hooks'; + +import { FolderDTO } from '../../../types'; + +import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; + +jest.mock('@grafana/runtime', () => { + const actual = jest.requireActual('@grafana/runtime'); + return { + ...actual, + getAppEvents: jest.fn(), + locationService: { + partial: jest.fn(), + }, + config: { + ...actual.config, + }, + }; +}); + +jest.mock('app/features/manage-dashboards/services/ValidationSrv', () => { + return { + validationSrv: { + validateNewFolderName: jest.fn(), + }, + }; +}); + +jest.mock('app/api/clients/provisioning', () => { + return { + useCreateRepositoryFilesWithPathMutation: jest.fn(), + }; +}); + +jest.mock('app/api/clients/folder', () => { + return { + useGetFolderQuery: jest.fn(), + }; +}); + +jest.mock('app/features/provisioning/hooks', () => { + return { + usePullRequestParam: jest.fn(), + useRepositoryList: jest.fn(), + }; +}); + +jest.mock('react-router-dom-v5-compat', () => { + const actual = jest.requireActual('react-router-dom-v5-compat'); + return { + ...actual, + useNavigate: () => jest.fn(), + }; +}); + +// Mock the defaults +jest.mock('../../dashboard-scene/saving/provisioned/defaults', () => { + return { + getDefaultWorkflow: jest.fn().mockReturnValue('write'), + getWorkflowOptions: jest.fn().mockReturnValue([ + { label: 'Commit directly', value: 'write' }, + { label: 'Create a branch', value: 'branch' }, + ]), + }; +}); + +interface Props { + onSubmit: () => void; + onCancel: () => void; + parentFolder: FolderDTO; +} + +function setup(props: Partial = {}) { + const user = userEvent.setup(); + + const defaultProps: Props = { + onSubmit: jest.fn(), + onCancel: jest.fn(), + parentFolder: { + id: 1, + uid: 'folder-uid', + title: 'Parent Folder', + url: '/dashboards/f/folder-uid', + hasAcl: false, + canSave: true, + canEdit: true, + canAdmin: true, + canDelete: true, + repository: { + name: 'test-repo', + type: 'github', + }, + } as unknown as FolderDTO, + ...props, + }; + + return { + user, + ...render(), + props: defaultProps, + }; +} + +const mockRequest = { + isSuccess: false, + isError: false, + isLoading: false, + error: null, + data: { resource: { upsert: { metadata: { name: 'new-folder' } } } }, +}; + +describe('NewProvisionedFolderForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + const mockAppEvents = { + publish: jest.fn(), + }; + (getAppEvents as jest.Mock).mockReturnValue(mockAppEvents); + + (useRepositoryList as jest.Mock).mockReturnValue([ + [ + { + metadata: { + name: 'test-repo', + }, + spec: { + title: 'Test Repository', + type: 'github', + github: { + url: 'https://github.com/grafana/grafana', + branch: 'main', + }, + workflows: [{ name: 'default', path: 'workflows/default.yaml' }], + }, + }, + ], + false, + ]); + + // Mock useGetFolderQuery + (useGetFolderQuery as jest.Mock).mockReturnValue({ + data: { + metadata: { + annotations: { + 'source.path': '/dashboards', + }, + }, + }, + isLoading: false, + isError: false, + }); + + // Mock usePullRequestParam + (usePullRequestParam as jest.Mock).mockReturnValue(null); + + // Mock useCreateRepositoryFilesWithPathMutation + const mockCreate = jest.fn(); + + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([mockCreate, mockRequest]); + + (validationSrv.validateNewFolderName as jest.Mock).mockResolvedValue(true); + }); + + it('should render the form with correct fields', () => { + setup(); + + // Check if form elements are rendered + expect(screen.getByRole('textbox', { name: /folder name/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /comment/i })).toBeInTheDocument(); + expect(screen.getByRole('radiogroup')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^create$/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + }); + + it('should show loading state when repository data is loading', () => { + (useRepositoryList as jest.Mock).mockReturnValue([[], true]); + + setup(); + + expect(screen.getByTestId('Spinner')).toBeInTheDocument(); + }); + + it('should show error when repository is not found', () => { + (useRepositoryList as jest.Mock).mockReturnValue([null, false]); + + setup(); + + expect(screen.getByText('Repository not found')).toBeInTheDocument(); + }); + + it('should show branch field when branch workflow is selected', async () => { + const { user } = setup(); + + expect(screen.queryByRole('textbox', { name: /branch/i })).not.toBeInTheDocument(); + + const branchOption = screen.getByRole('radio', { name: /create a branch/i }); + await user.click(branchOption); + + expect(screen.getByRole('textbox', { name: /branch/i })).toBeInTheDocument(); + }); + + it('should validate folder name', async () => { + (validationSrv.validateNewFolderName as jest.Mock).mockRejectedValue(new Error('Folder name already exists')); + + const { user } = setup(); + + const folderNameInput = screen.getByRole('textbox', { name: /folder name/i }); + await user.clear(folderNameInput); + await user.type(folderNameInput, 'Existing Folder'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: /^create$/i }); + await user.click(submitButton); + + // Wait for validation error to appear + await waitFor(() => { + expect(screen.getByText('Folder name already exists')).toBeInTheDocument(); + }); + }); + + it('should validate branch name', async () => { + const { user } = setup(); + + // Select branch workflow + const branchOption = screen.getByRole('radio', { name: /create a branch/i }); + await user.click(branchOption); + + // Enter invalid branch name + const branchInput = screen.getByRole('textbox', { name: /branch/i }); + await user.clear(branchInput); + await user.type(branchInput, 'invalid//branch'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: /^create$/i }); + await user.click(submitButton); + + // Wait for validation error to appear + await waitFor(() => { + expect(screen.getByText('Invalid branch name.')).toBeInTheDocument(); + }); + }); + + it('should create folder successfully', async () => { + const mockCreate = jest.fn(); + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + mockCreate, + { + ...mockRequest, + isSuccess: true, + isError: false, + isLoading: false, + error: null, + }, + ]); + + const { user, props } = setup(); + + const folderNameInput = screen.getByRole('textbox', { name: /folder name/i }); + const commentInput = screen.getByRole('textbox', { name: /comment/i }); + + await user.clear(folderNameInput); + await user.type(folderNameInput, 'New Test Folder'); + + await user.clear(commentInput); + await user.type(commentInput, 'Creating a new test folder'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: /^create$/i }); + await user.click(submitButton); + + // Check if create was called with correct parameters + await waitFor(() => { + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + ref: undefined, // write workflow uses undefined ref + name: 'test-repo', + message: 'Creating a new test folder', + body: { + title: 'New Test Folder', + type: 'folder', + }, + }) + ); + }); + + // Check if onSubmit was called + expect(props.onSubmit).toHaveBeenCalled(); + }); + + it('should create folder with branch workflow', async () => { + const mockCreate = jest.fn(); + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + mockCreate, + { + ...mockRequest, + isSuccess: true, + isError: false, + isLoading: false, + error: null, + }, + ]); + + const { user } = setup(); + + // Fill form + const folderNameInput = screen.getByRole('textbox', { name: /folder name/i }); + await user.clear(folderNameInput); + await user.type(folderNameInput, 'Branch Folder'); + + // Select branch workflow + const branchOption = screen.getByRole('radio', { name: /create a branch/i }); + await user.click(branchOption); + + // Enter branch name + const branchInput = screen.getByRole('textbox', { name: /branch/i }); + await user.clear(branchInput); + await user.type(branchInput, 'feature/new-folder'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: /^create$/i }); + await user.click(submitButton); + + // Check if create was called with correct parameters + await waitFor(() => { + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + ref: 'feature/new-folder', + name: 'test-repo', + message: 'Create folder: Branch Folder', + body: { + title: 'Branch Folder', + type: 'folder', + }, + }) + ); + }); + }); + + it('should show error when folder creation fails', async () => { + const mockCreate = jest.fn(); + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + mockCreate, + { + ...mockRequest, + isSuccess: false, + isError: true, + isLoading: false, + error: 'Failed to create folder', + }, + ]); + + const { user } = setup(); + + // Fill form + const folderNameInput = screen.getByRole('textbox', { name: /folder name/i }); + await user.clear(folderNameInput); + await user.type(folderNameInput, 'Error Folder'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: /^create$/i }); + await user.click(submitButton); + + // Check if error alert was published + await waitFor(() => { + const appEvents = getAppEvents(); + expect(appEvents.publish).toHaveBeenCalledWith({ + type: AppEvents.alertError.name, + payload: ['Error creating folder', 'Failed to create folder'], + }); + }); + }); + + it('should disable create button when form is submitting', async () => { + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + jest.fn(), + { + ...mockRequest, + isSuccess: false, + isError: false, + isLoading: true, + error: null, + }, + ]); + + setup(); + + // Create button should be disabled and show loading text + const createButton = screen.getByRole('button', { name: /creating/i }); + expect(createButton).toBeDisabled(); + expect(createButton).toHaveTextContent('Creating...'); + }); + + it('should show PR link when PR URL is available', () => { + (usePullRequestParam as jest.Mock).mockReturnValue('https://github.com/grafana/grafana/pull/1234'); + + setup(); + + // PR alert should be visible - use text content instead of role + expect(screen.getByText('Pull request created')).toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveTextContent('https://github.com/grafana/grafana/pull/1234'); + }); + + it('should call onCancel when cancel button is clicked', async () => { + const { user, props } = setup(); + + // Click cancel button + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + await user.click(cancelButton); + + // Check if onCancel was called + expect(props.onCancel).toHaveBeenCalled(); + }); + + it('should show read-only alert when repository has no workflows', () => { + // Mock repository with empty workflows array + (useRepositoryList as jest.Mock).mockReturnValue([ + [ + { + metadata: { + name: 'test-repo', + }, + spec: { + type: 'github', + github: { + url: 'https://github.com/grafana/grafana', + branch: 'main', + }, + workflows: [], // Empty workflows array + }, + }, + ], + false, + ]); + + setup(); + + // Read-only alert should be visible + expect(screen.getByText('This repository is read only')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx new file mode 100644 index 00000000000..5414e5ac9c3 --- /dev/null +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -0,0 +1,249 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { AppEvents } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { Alert, Button, Field, Input, RadioButtonGroup, Spinner, Stack, TextArea } from '@grafana/ui'; +import { useGetFolderQuery } from 'app/api/clients/folder'; +import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning'; +import { AnnoKeyManagerIdentity, AnnoKeySourcePath, Resource } from 'app/features/apiserver/types'; +import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults'; +import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; +import { PROVISIONING_URL } from 'app/features/provisioning/constants'; +import { usePullRequestParam, useRepositoryList } from 'app/features/provisioning/hooks'; +import { WorkflowOption } from 'app/features/provisioning/types'; +import { validateBranchName } from 'app/features/provisioning/utils/git'; +import { FolderDTO } from 'app/types'; + +type FormData = { + ref?: string; + path: string; + comment?: string; + repo: string; + workflow?: WorkflowOption; + title: string; +}; + +interface Props { + onSubmit: () => void; + onCancel: () => void; + parentFolder?: FolderDTO; +} + +const initialFormValues: Partial = { + title: '', + comment: '', + ref: `folder/${Date.now()}`, +}; + +export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: Props) { + const [items, isLoading] = useRepositoryList(); + const prURL = usePullRequestParam(); + const navigate = useNavigate(); + const [create, request] = useCreateRepositoryFilesWithPathMutation(); + + // Get k8s folder data, necessary to get parent folder path + const folderQuery = useGetFolderQuery(parentFolder ? { name: parentFolder.uid } : skipToken); + const repositoryName = folderQuery.data?.metadata?.annotations?.[AnnoKeyManagerIdentity]; + if (!items && !isLoading) { + return ; + } + + const repository = repositoryName ? items?.find((item) => item?.metadata?.name === repositoryName) : items?.[0]; + const repositoryConfig = repository?.spec; + const isGitHub = Boolean(repositoryConfig?.github); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + control, + setValue, + } = useForm({ defaultValues: { ...initialFormValues, workflow: getDefaultWorkflow(repositoryConfig) } }); + + const [workflow, ref] = watch(['workflow', 'ref']); + + useEffect(() => { + setValue('workflow', getDefaultWorkflow(repositoryConfig)); + }, [repositoryConfig, setValue]); + + useEffect(() => { + const appEvents = getAppEvents(); + if (request.isSuccess) { + onSubmit(); + + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Folder created successfully'], + }); + + const folder = request.data.resource?.upsert as Resource; + if (folder?.metadata?.name) { + navigate(`/dashboards/f/${folder?.metadata?.name}/`); + return; + } + + let url = `${PROVISIONING_URL}/${repositoryName}/file/${request.data.path}`; + if (request.data.ref?.length) { + url += '?ref=' + request.data.ref; + } + navigate(url); + } else if (request.isError) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: ['Error creating folder', request.error], + }); + } + }, [ + request.isSuccess, + request.isError, + request.error, + onSubmit, + ref, + request.data, + workflow, + navigate, + repositoryName, + ]); + + if (isLoading || folderQuery.isLoading) { + return ; + } + + const validateFolderName = async (folderName: string) => { + try { + await validationSrv.validateNewFolderName(folderName); + return true; + } catch (e) { + if (e instanceof Error) { + return e.message; + } + return 'Invalid folder name'; + } + }; + + const doSave = async ({ ref, title, workflow, comment }: FormData) => { + const repoName = repository?.metadata?.name; + if (!title || !repoName) { + return; + } + const basePath = folderQuery.data?.metadata?.annotations?.[AnnoKeySourcePath] ?? ''; + + // Convert folder title to filename format (lowercase, replace spaces with hyphens) + const titleInFilenameFormat = title + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, ''); + + const prefix = basePath ? `${basePath}/` : ''; + const path = `${prefix}${titleInFilenameFormat}/`; + + const folderModel = { + title, + type: 'folder', + }; + + if (workflow === 'write') { + ref = undefined; + } + + create({ + ref, + name: repoName, + path, + message: comment || `Create folder: ${title}`, + body: folderModel, + }); + }; + + return ( + + + {!repositoryConfig?.workflows.length && ( + + If you have direct access to the target, copy the JSON and paste it there. + + )} + + + + + + +