diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index 141ad441208..81b97bb03f3 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -6,18 +6,20 @@ const jsxA11yPlugin = require('eslint-plugin-jsx-a11y'); const lodashPlugin = require('eslint-plugin-lodash'); const barrelPlugin = require('eslint-plugin-no-barrel-files'); const reactPlugin = require('eslint-plugin-react'); -const hooksPlugin = require('eslint-plugin-react-hooks'); const testingLibraryPlugin = require('eslint-plugin-testing-library'); const grafanaConfig = require('@grafana/eslint-config/flat'); const grafanaPlugin = require('@grafana/eslint-plugin'); const grafanaI18nPlugin = require('@grafana/i18n/eslint-plugin'); -// Include the Grafana config and remove the rules, +// Include the base Grafana configs and remove the rules, // as we just want to pull in all of the necessary configuration but not run the rules // (this should only be concerned with checking rules that we want to improve, // so there's no need to try and run the rules that will be linted properly anyway) -const { rules, ...baseConfig } = grafanaConfig.find((config) => config?.name === '@grafana/eslint-config/flat'); +const mappedBaseConfigs = grafanaConfig.map((config) => { + const { rules, ...baseConfig } = config; + return baseConfig; +}); /** * @type {Array} @@ -50,22 +52,13 @@ module.exports = [ ], }, { - name: 'react/jsx-runtime', - // @ts-ignore - not sure why but flat config is typed as a maybe? - ...reactPlugin.configs.flat['jsx-runtime'], - }, - // FIXME: Remove once eslint-config-grafana is updated to include correct plugin - { - name: 'react-hooks-plugin', - plugins: { - 'react-hooks': hooksPlugin, - }, + name: 'react/jsx-runtime-rules', + rules: reactPlugin.configs.flat['jsx-runtime'].rules, }, + ...mappedBaseConfigs, { files: ['**/*.{ts,tsx,js}'], - ...baseConfig, plugins: { - ...baseConfig.plugins, '@emotion': emotionPlugin, lodash: lodashPlugin, jest: jestPlugin, @@ -85,6 +78,7 @@ module.exports = [ { files: ['**/*.{js,jsx,ts,tsx}'], rules: { + 'react-hooks/rules-of-hooks': 'error', '@typescript-eslint/no-explicit-any': 'error', '@grafana/no-aria-label-selectors': 'error', 'no-restricted-imports': [ diff --git a/.betterer.results b/.betterer.results index 7712f1a83a9..6eb58950612 100644 --- a/.betterer.results +++ b/.betterer.results @@ -548,6 +548,16 @@ exports[`better eslint`] = { "packages/grafana-runtime/src/utils/queryResponse.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "packages/grafana-runtime/src/utils/useFavoriteDatasources.ts:5381": [ + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "2"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "3"], + [0, 0, 0, "React Hook \\"useEffect\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "4"], + [0, 0, 0, "React Hook \\"useState\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "5"], + [0, 0, 0, "React Hook \\"useState\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "6"], + [0, 0, 0, "React Hook \\"useState\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "7"] + ], "packages/grafana-runtime/src/utils/userStorage.tsx:5381": [ [0, 0, 0, "Direct usage of localStorage is not allowed. import store from @grafana/data instead", "0"], [0, 0, 0, "Direct usage of localStorage is not allowed. import store from @grafana/data instead", "1"], @@ -596,6 +606,20 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "packages/grafana-ui/src/components/Combobox/Combobox.story.tsx:5381": [ + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "2"], + [0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "3"], + [0, 0, 0, "React Hook \\"useState\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "4"] + ], + "packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx:5381": [ + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "2"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "3"], + [0, 0, 0, "React Hook \\"useArgs\\" is called in function \\"render\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "4"] + ], "packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -616,6 +640,33 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "packages/grafana-ui/src/components/ElementSelectionContext/ElementSelectionContext.tsx:5381": [ + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"], + [0, 0, 0, "React Hook \\"useContext\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "2"] + ], + "packages/grafana-ui/src/components/Forms/Form.story.tsx:5381": [ + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "2"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "3"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "4"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "5"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "6"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "7"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"renderForm\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "8"] + ], + "packages/grafana-ui/src/components/Forms/InlineField.story.tsx:5381": [ + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"basic: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"error: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"grow: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "2"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"multiple: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "3"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"multiple: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "4"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"multiple: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "5"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"withCombobox: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "6"], + [0, 0, 0, "React Hook \\"useId\\" is called in function \\"withTooltip: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "7"], + [0, 0, 0, "React Hook \\"useState\\" is called in function \\"withCombobox: StoryFn\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "8"] + ], "packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -864,7 +915,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/api/clients/folder/v1beta1/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "React Hook \\"useGetDisplayMappingQuery\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"], + [0, 0, 0, "React Hook \\"useGetFolderParentsQuery\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "2"], + [0, 0, 0, "React Hook \\"useGetFolderQuery\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "3"] ], "public/app/core/TableModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -1274,6 +1328,9 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] ], + "public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx:5381": [ + [0, 0, 0, "React Hook \\"useStyles2\\" is called in function \\"getPreviewResults\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], "public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -1403,6 +1460,9 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rule-editor/rule-types/RuleType.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], + "public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx:5381": [ + [0, 0, 0, "React Hook \\"useStyles2\\" is called in function \\"createMetadata\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -1554,6 +1614,18 @@ exports[`better eslint`] = { "public/app/features/browse-dashboards/components/NewFolderForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], + "public/app/features/commandPalette/actions/recentScopesActions.ts:5381": [ + [0, 0, 0, "React Hook \\"useScopesServices\\" is called in function \\"getRecentScopesActions\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], + "public/app/features/commandPalette/actions/scopeActions.tsx:5381": [ + [0, 0, 0, "React Hook \\"useGlobalScopesSearch\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"], + [0, 0, 0, "React Hook \\"useRegisterActions\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"], + [0, 0, 0, "React Hook \\"useScopeTreeActions\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "2"], + [0, 0, 0, "React Hook \\"useScopesRow\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "3"] + ], + "public/app/features/commandPalette/actions/scopesUtils.ts:5381": [ + [0, 0, 0, "React Hook \\"useObservable\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"] + ], "public/app/features/connections/components/ConnectionsRedirectNotice/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] ], @@ -1633,9 +1705,23 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx:5381": [ + [0, 0, 0, "React Hook \\"useEffect\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"], + [0, 0, 0, "React Hook \\"useEffect\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "1"], + [0, 0, 0, "React Hook \\"useSceneObjectState\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "2"], + [0, 0, 0, "React Hook \\"useSnappingSplitter\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"] + ], + "public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx:5381": [ + [0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + ], "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx:5381": [ + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + ], "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -1645,7 +1731,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] ], "public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -1662,7 +1749,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useToggle\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"] ], "public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] @@ -1673,6 +1762,9 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"] ], + "public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx:5381": [ + [0, 0, 0, "React Hook \\"useIsProvisionedNG\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + ], "public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], @@ -1712,21 +1804,39 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"] ], + "public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx:5381": [ + [0, 0, 0, "React Hook \\"useConditionalRenderingEditor\\" is called in function \\"getOptions\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], "public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"] ], + "public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx:5381": [ + [0, 0, 0, "React Hook \\"useConditionalRenderingEditor\\" is called in function \\"getDashboardGridItemOptions\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], + "public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx:5381": [ + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + ], "public/app/features/dashboard-scene/scene/layout-default/row-actions/RowOptionsForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] ], + "public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx:5381": [ + [0, 0, 0, "React Hook \\"useEditOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + ], "public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useConditionalRenderingEditor\\" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + ], + "public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx:5381": [ + [0, 0, 0, "React Hook \\"useEditOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] ], "public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useConditionalRenderingEditor\\" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.", "1"] ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -1751,6 +1861,15 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx:5381": [ + [0, 0, 0, "React Hook \\"useDashboardEditPageNav\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + ], + "public/app/features/dashboard-scene/settings/JsonModelEditView.tsx:5381": [ + [0, 0, 0, "React Hook \\"useDashboardEditPageNav\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useSaveDashboard\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useState\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"] + ], "public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -1772,11 +1891,19 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "8"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "9"] ], + "public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx:5381": [ + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + ], "public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], + [0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"] ], "public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] ], "public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] @@ -2231,6 +2358,10 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] ], + "public/app/features/datasources/components/EditDataSource.test.tsx:5381": [ + [0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], + [0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"] + ], "public/app/features/datasources/components/picker/DataSourceCard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], @@ -2311,9 +2442,16 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx:5381": [ + [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"], + [0, 0, 0, "React Hook \\"usePluginLinks\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "1"] + ], "public/app/features/explore/TraceView/components/TracePageHeader/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./TracePageHeader\`)", "0"] ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx:5381": [ + [0, 0, 0, "React Hook \\"usePluginLinks\\" is called in function \\"getSpanDetailLinkButtons\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"] + ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx:5381": [ [0, 0, 0, "Using localeCompare() can cause performance issues when sorting large datasets. Consider using Intl.Collator for better performance when sorting arrays, or add an eslint-disable comment if sorting a small, known dataset.", "0"] ], @@ -2424,6 +2562,19 @@ exports[`better eslint`] = { "public/app/features/live/centrifuge/channel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/logs/components/panel/LogLineDetailsFields.tsx:5381": [ + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "2"], + [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"], + [0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "4"], + [0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "5"] + ], + "public/app/features/logs/components/panel/LogList.tsx:5381": [ + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"], + [0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "2"] + ], "public/app/features/logs/logsFrame.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2615,14 +2766,24 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/provisioning/GettingStarted/Sidebar.tsx:5381": [ + [0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"] + ], "public/app/features/provisioning/GettingStarted/SidebarItem.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], + "public/app/features/provisioning/Job/JobContent.tsx:5381": [ + [0, 0, 0, "React Hook \\"useEffect\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"] + ], + "public/app/features/provisioning/Job/RecentJobs.tsx:5381": [ + [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "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"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"] + [0, 0, 0, "React Hook \\"usePluginComponents\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"], + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"] ], "public/app/features/query/components/QueryEditorRowHeader.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] @@ -2793,6 +2954,9 @@ exports[`better eslint`] = { "public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], + "public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx:5381": [ + [0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"] + ], "public/app/features/transformers/editors/ReduceTransformerEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/.bingo/.gitignore b/.bingo/.gitignore deleted file mode 100644 index 9efccf683cd..00000000000 --- a/.bingo/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ - -# Ignore everything -* - -# But not these files: -!.gitignore -!*.mod -!*.sum -!README.md -!Variables.mk -!variables.env - -*tmp.mod diff --git a/.bingo/README.md b/.bingo/README.md deleted file mode 100644 index 1d8a1360ccf..00000000000 --- a/.bingo/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Project Development Dependencies. - -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. - -## Requirements - -- Go 1.14+ diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk deleted file mode 100644 index 4160da8e0f7..00000000000 --- a/.bingo/Variables.mk +++ /dev/null @@ -1,25 +0,0 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. -# All tools are designed to be build inside $GOBIN. -BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) -GOPATH ?= $(shell go env GOPATH) -GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin -GO ?= $(shell which go) - -# 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 drone variable: -# -# In your main Makefile (for non array binaries): -# -#include .bingo/Variables.mk # Assuming -dir was set to .bingo . -# -#command: $(DRONE) -# @echo "Running drone" -# @$(DRONE) -# -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" - diff --git a/.bingo/drone.mod b/.bingo/drone.mod deleted file mode 100644 index bb76a5ebc7d..00000000000 --- a/.bingo/drone.mod +++ /dev/null @@ -1,7 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.17 - -replace github.com/docker/docker => github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible - -require github.com/drone/drone-cli v1.5.0 // drone CGO_ENABLED=0 diff --git a/.bingo/drone.sum b/.bingo/drone.sum deleted file mode 100644 index b6a6118b691..00000000000 --- a/.bingo/drone.sum +++ /dev/null @@ -1,1010 +0,0 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -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/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/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/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= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= -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/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -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/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -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/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -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.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -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/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -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/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw= -github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -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.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= -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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible h1:hx8H7MbcmXUXAmphQuA/XB7CfSzX4DRrNuHFvfK9aIQ= -github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= -github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o= -github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/drone/drone-cli v1.5.0 h1:dAoU2YBs7c0XjuDs2ql0W7x0+dOvJ/KLh5UrxUuaOnk= -github.com/drone/drone-cli v1.5.0/go.mod h1:682hzP+pG/nS7Xu6YJy064tUrbG7jTpB3gdy0kdxIOM= -github.com/drone/drone-go v1.7.0 h1:oEFWVcagBmAkVuFBpBq9lImZX1caDM+zRsmC4O1vXgQ= -github.com/drone/drone-go v1.7.0/go.mod h1:fxCf9jAnXDZV1yDr0ckTuWd1intvcQwfJmTRpTZ1mXg= -github.com/drone/drone-runtime v1.0.7-0.20190729070836-38f28a11afe8/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= -github.com/drone/drone-runtime v1.1.1-0.20200623162453-61e33e2cab5d h1:P5HI/Y9hARTZ3F3EKs0kYijhjXZWQRQHYn1neTi0pWM= -github.com/drone/drone-runtime v1.1.1-0.20200623162453-61e33e2cab5d/go.mod h1:4/2QToW5+HGD0y1sTw7X35W1f7YINS14UfDY4isggT8= -github.com/drone/drone-yaml v0.0.0-20190729072335-70fa398b3560 h1:3QL4NnDpGtaXpgI9eNd6N2k5WK8W388CzD67ZTuuZQg= -github.com/drone/drone-yaml v0.0.0-20190729072335-70fa398b3560/go.mod h1:rCLISp/rqZ50s6G4nKsm971tRSzolxzqqXfgjDqPYoE= -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-20190918184546-d4ef6e88376d h1:/IO7UVVu191Jc0DajV4cDVoO+91cuppvgxg2MZl+AXI= -github.com/drone/funcmap v0.0.0-20190918184546-d4ef6e88376d/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= -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/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -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/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -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-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -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-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-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= -github.com/gogo/protobuf v0.0.0-20170307180453-100ba4e88506/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -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/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -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/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 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -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.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-jsonnet v0.17.0 h1:/9NIEfhK1NQRKl3sP2536b2+x5HnZMdql7x3yK/l8JY= -github.com/google/go-jsonnet v0.17.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= -github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.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/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -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/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -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.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -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= -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/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -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.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -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/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -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/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/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.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c/go.mod h1:HUpKUBZnpzkdx0kD/+Yfuft+uD3zHGtXF/XJB14TUr4= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -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/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -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/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.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/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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= -github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -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.starlark.net v0.0.0-20201118183435-e55f603d8c79 h1:JPjLPz44y2N9mkzh2N344kTk1Y4/V4yJAjTrXGmzv8I= -go.starlark.net v0.0.0-20201118183435-e55f603d8c79/go.mod h1:5YFcFnRptTN+41758c2bMPiqpGg4zBfYji1IQz8wNFk= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/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-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -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/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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/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-20181220203305-927f97764cc3/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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -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-20190619014844-b5b0513f8c1b/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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/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-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -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-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 h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= -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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190222072716-a9d3bda3a223/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-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/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-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/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-20200217220822-9197077df867/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/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-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -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/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -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/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190328211700-ab21143f2384/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-20190614205625-5aca471b1d59/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-20190624222133-a101b041ded4/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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -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/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -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.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -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-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -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-20200117163144-32f20d992d24/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-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -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.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -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.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -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.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -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-20141024133853-64131543e789/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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -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= -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.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= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -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= -k8s.io/api v0.0.0-20181130031204-d04500c8c3dd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.0.0-20181201231028-18a5ff3097b4/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -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= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/.bingo/go.mod b/.bingo/go.mod deleted file mode 100644 index 610249af0b0..00000000000 --- a/.bingo/go.mod +++ /dev/null @@ -1 +0,0 @@ -module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files. \ No newline at end of file diff --git a/.bingo/variables.env b/.bingo/variables.env deleted file mode 100644 index b4e2c715f3e..00000000000 --- a/.bingo/variables.env +++ /dev/null @@ -1,12 +0,0 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. -# All tools are designed to be build inside $GOBIN. -# Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. -GOBIN=${GOBIN:=$(go env GOBIN)} - -if [ -z "$GOBIN" ]; then - GOBIN="$(go env GOPATH)/bin" -fi - - -DRONE="${GOBIN}/drone-v1.5.0" - diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2dac93d87e7..a8f94cbc472 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -58,7 +58,6 @@ /go.sum @grafana/grafana-backend-group /go.work @grafana/grafana-app-platform-squad /go.work.sum @grafana/grafana-app-platform-squad -/.bingo/ @grafana/grafana-backend-group /.citools @grafana/grafana-developer-enablement-squad /pkg/README.md @grafana/grafana-backend-group /pkg/ruleguard.rules.go @grafana/grafana-backend-group diff --git a/.github/actions/change-detection/action.yml b/.github/actions/change-detection/action.yml index e7606f453e1..67b63bfe363 100644 --- a/.github/actions/change-detection/action.yml +++ b/.github/actions/change-detection/action.yml @@ -98,7 +98,6 @@ runs: - 'proto/**' - '**/Makefile' - 'scripts/**' - - '!scripts/drone/**' - '!**.md' - '.github/actions/change-detection/**' - '**.cue' @@ -123,7 +122,6 @@ runs: - 'scripts/**' - '!**.md' - '.citools/**' - - '.bingo/**' - '.github/actions/change-detection/**' - '${{ inputs.self }}' docs: diff --git a/.github/pr-commands.json b/.github/pr-commands.json index f97a0738700..8b4472fe02a 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -71,16 +71,6 @@ "action": "updateLabel", "addLabel": "type/build-packaging" }, - { - "type": "changedfiles", - "matches": [ - "scripts/*.star", - ".drone.star", - ".drone.yml" - ], - "action": "updateLabel", - "addLabel": "type/ci" - }, { "type": "changedfiles", "matches": [ diff --git a/.github/workflows/backport-workflow.yml b/.github/workflows/backport-workflow.yml index 925c6c54bb3..1e57a95fed2 100644 --- a/.github/workflows/backport-workflow.yml +++ b/.github/workflows/backport-workflow.yml @@ -13,7 +13,7 @@ permissions: {} jobs: backport: # Only run this job if the triggering workflow was not skipped (and on grafana repo) - if: github.repository == 'grafana/grafana' && github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.head_repository.fork == false && github.repository == 'grafana/grafana' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest permissions: id-token: write diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index eea0caa319c..17f61de43f1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,28 @@ permissions: security-events: write jobs: + detect-changes: + name: Detect whether code changed + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + javascript: ${{ steps.detect-changes.outputs.frontend }} + go: ${{ steps.detect-changes.outputs.backend }} + actions: 'true' + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true # required to get more history in the changed-files action + fetch-depth: 2 + - name: Detect changes + id: detect-changes + uses: ./.github/actions/change-detection + with: + self: .github/workflows/codeql-analysis.yml + analyze: + needs: detect-changes name: Analyze runs-on: ubuntu-latest continue-on-error: true # doesn't block PRs from being merged if this fails @@ -41,6 +62,7 @@ jobs: steps: - name: Checkout repository + if: needs.detect-changes.outputs[matrix.language] == 'true' uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is @@ -48,7 +70,7 @@ jobs: fetch-depth: 2 persist-credentials: false - - if: matrix.language == 'go' + - if: matrix.language == 'go' && needs.detect-changes.outputs.go == 'true' name: Set go version uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: @@ -56,6 +78,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL + if: needs.detect-changes.outputs[matrix.language] == 'true' uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} @@ -64,7 +87,7 @@ jobs: # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - - if: matrix.language == 'go' + - if: matrix.language == 'go' && needs.detect-changes.outputs.go == 'true' name: Build go files run: | go mod verify diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 322160a5665..ba57ca81d23 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -10,7 +10,7 @@ on: schedule: # Every weeknight at midnight # "Scheduled workflows will only run on the default branch." (docs.github.com) - - cron: "0 0 * * 1-5" + - cron: '0 0 * * 1-5' push: branches: - release-*.*.* @@ -108,7 +108,7 @@ jobs: github-token: ${{ steps.generate_token.outputs.token }} script: | const {REF, VERSION, BUILD_ID, BUCKET, GRAFANA_COMMIT, SOURCE_EVENT} = process.env; - + await github.rest.actions.createWorkflowDispatch({ owner: 'grafana', repo: 'grafana-enterprise', @@ -166,7 +166,7 @@ jobs: artifacts: targz:grafana:darwin/arm64 verify: true steps: - - uses: grafana/shared-workflows/actions/dockerhub-login@main + - uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2 - uses: actions/checkout@v4 with: persist-credentials: false @@ -209,3 +209,60 @@ jobs: run-id: ${{ github.run_id }} bucket-path: ${{ needs.setup.outputs.version }}_${{ github.run_id }} environment: prod + publish-dockerhub: + if: github.ref_name == 'main' + permissions: + contents: read + id-token: write + runs-on: ubuntu-x64-small + needs: + - setup + - build + steps: + - uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-list-linux-amd64 + path: . + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-list-linux-arm64 + path: . + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-list-linux-armv7 + path: . + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-linux-amd64 + path: dist + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-linux-arm64 + path: dist + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: artifacts-linux-armv7 + path: dist + - name: Push to Docker Hub + env: + VERSION: ${{ needs.setup.outputs.version }} + run: | + # grep can use a wildcard but then it includes the filename as part of the result and that gets complicated. + # It's easier to use cat to combine the artifact lists + cat artifacts-*.txt > artifacts.txt + grep 'grafana_.*docker.tar.gz$' artifacts.txt | xargs -I % docker load -i % | sed 's/Loaded image: //g' | tee docker_images + while read -r line; do + # This tag will be `grafana/grafana-image-tags:...` + docker push "$line" + done < docker_images + + docker manifest create grafana/grafana:main "grafana/grafana-image-tags:${VERSION}-amd64" "grafana/grafana-image-tags:${VERSION}-arm64" "grafana/grafana-image-tags:${VERSION}-armv7" + docker manifest create grafana/grafana:main-ubuntu "grafana/grafana-image-tags:${VERSION}-ubuntu-amd64" "grafana/grafana-image-tags:${VERSION}-ubuntu-arm64" "grafana/grafana-image-tags:${VERSION}-ubuntu-armv7" + docker manifest create "grafana/grafana-dev:${VERSION}" "grafana/grafana-image-tags:${VERSION}-amd64" "grafana/grafana-image-tags:${VERSION}-arm64" "grafana/grafana-image-tags:${VERSION}-armv7" + docker manifest create "grafana/grafana-dev:${VERSION}-ubuntu" "grafana/grafana-image-tags:${VERSION}-ubuntu-amd64" "grafana/grafana-image-tags:${VERSION}-ubuntu-arm64" "grafana/grafana-image-tags:${VERSION}-ubuntu-armv7" + + docker manifest push grafana/grafana:main + docker manifest push grafana/grafana:main-ubuntu + docker manifest push "grafana/grafana-dev:${VERSION}" + docker manifest push "grafana/grafana-dev:${VERSION}-ubuntu" diff --git a/Dockerfile b/Dockerfile index bb4fe895684..618779f7305 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,6 @@ ARG COMMIT_SHA="" ARG BUILD_BRANCH="" ARG GO_BUILD_TAGS="oss" ARG WIRE_TAGS="oss" -ARG BINGO="true" RUN if grep -i -q alpine /etc/issue; then \ apk add --no-cache \ @@ -76,7 +75,6 @@ RUN if grep -i -q alpine /etc/issue; then \ WORKDIR /tmp/grafana COPY go.* ./ -COPY .bingo .bingo COPY .citools .citools # Copy go dependencies first @@ -109,10 +107,6 @@ COPY pkg/codegen pkg/codegen COPY pkg/plugins/codegen pkg/plugins/codegen RUN go mod download -RUN if [[ "$BINGO" = "true" ]]; then \ - go install github.com/bwplotka/bingo@latest && \ - bingo get -v; \ - fi COPY embed.go Makefile build.go package.json ./ COPY cue.mod cue.mod diff --git a/Makefile b/Makefile index ab99a49fc24..92c20458a6f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ WIRE_TAGS = "oss" -include local/Makefile -include .bingo/Variables.mk include .citools/Variables.mk GO = go @@ -200,7 +199,7 @@ gen-enterprise-go: ## Generate Wire graph (Enterprise) $(GO) run ./pkg/build/wire/cmd/wire/main.go gen -tags "enterprise" -gen_tags "(enterprise || pro)" -output_file_prefix="enterprise_" ./pkg/server endif gen-go: gen-enterprise-go ## Generate Wire graph - @echo "generatng Wire graph" + @echo "generating Wire graph" $(GO) run ./pkg/build/wire/cmd/wire/main.go gen -tags "oss" -gen_tags "(!enterprise && !pro)" ./pkg/server .PHONY: fix-cue @@ -421,7 +420,6 @@ build-docker-full: ## Build Docker image for development. tar -ch . | \ docker buildx build - \ --platform $(PLATFORM) \ - --build-arg BINGO=false \ --build-arg NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \ --build-arg JS_NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \ --build-arg JS_YARN_INSTALL_FLAG=$(DOCKER_JS_YARN_INSTALL_FLAG) \ @@ -439,7 +437,6 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. tar -ch . | \ docker buildx build - \ --platform $(PLATFORM) \ - --build-arg BINGO=false \ --build-arg NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \ --build-arg JS_NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \ --build-arg JS_YARN_INSTALL_FLAG=$(DOCKER_JS_YARN_INSTALL_FLAG) \ diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index c6df95d196f..3cf8a768483 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -5,7 +5,7 @@ import ( ) const ( - MIN_VERSION = 14 + MIN_VERSION = 13 LATEST_VERSION = 41 ) @@ -38,6 +38,7 @@ type PanelPluginInfoProvider interface { func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 14: V14, 15: V15, 16: V16, 17: V17, diff --git a/apps/dashboard/pkg/migration/schemaversion/v14.go b/apps/dashboard/pkg/migration/schemaversion/v14.go new file mode 100644 index 00000000000..6254dc2e1b6 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v14.go @@ -0,0 +1,38 @@ +package schemaversion + +// V14 migrates the sharedCrosshair boolean property to graphTooltip integer property. +// This migration converts the old boolean shared crosshair setting to the new integer-based +// graph tooltip setting for consistency with updated dashboard tooltip behavior. + +// Example before migration: +// { +// "schemaVersion": 13, +// "title": "My Dashboard", +// "sharedCrosshair": true, +// "panels": [...] +// } + +// Example after migration: +// { +// "schemaVersion": 14, +// "title": "My Dashboard", +// "graphTooltip": 1, +// "panels": [...] +// } + +func V14(dashboard map[string]interface{}) error { + // Convert sharedCrosshair boolean to graphTooltip integer + sharedCrosshair := GetBoolValue(dashboard, "sharedCrosshair") + + if sharedCrosshair { + dashboard["graphTooltip"] = 1 + } else { + dashboard["graphTooltip"] = 0 + } + + // Remove the old sharedCrosshair property + delete(dashboard, "sharedCrosshair") + + dashboard["schemaVersion"] = 14 + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v14_test.go b/apps/dashboard/pkg/migration/schemaversion/v14_test.go new file mode 100644 index 00000000000..196f2448a68 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v14_test.go @@ -0,0 +1,93 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV14(t *testing.T) { + tests := []migrationTestCase{ + { + name: "v14 migration converts sharedCrosshair true to graphTooltip 1", + input: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 13, + "sharedCrosshair": true, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "CPU Usage", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 14, + "graphTooltip": 1, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "CPU Usage", + "id": 1, + }, + }, + }, + }, + { + name: "v14 migration converts sharedCrosshair false to graphTooltip 0", + input: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 13, + "sharedCrosshair": false, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Memory Usage", + "id": 2, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 14, + "graphTooltip": 0, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Memory Usage", + "id": 2, + }, + }, + }, + }, + { + name: "v14 migration handles missing sharedCrosshair (defaults to false/0)", + input: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 13, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "title": "Server Stats", + "id": 3, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V14 Migration Test Dashboard", + "schemaVersion": 14, + "graphTooltip": 0, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "title": "Server Stats", + "id": 3, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V14) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v14.shared_crosshair_to_graph_tooltip.json b/apps/dashboard/pkg/migration/testdata/input/v14.shared_crosshair_to_graph_tooltip.json new file mode 100644 index 00000000000..ea2d32f7aac --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v14.shared_crosshair_to_graph_tooltip.json @@ -0,0 +1,65 @@ +{ + "title": "V14 Shared Crosshair Migration Test Dashboard", + "schemaVersion": 13, + "sharedCrosshair": true, + "panels": [ + { + "type": "graph", + "title": "CPU Usage Over Time", + "id": 1, + "targets": [ + { + "expr": "cpu_usage", + "refId": "A" + } + ], + "yAxes": [ + { + "show": true, + "min": null, + "max": null + } + ] + }, + { + "type": "graph", + "title": "Memory Usage", + "id": 2, + "targets": [ + { + "expr": "memory_usage", + "refId": "B" + } + ], + "yAxes": [ + { + "show": true, + "min": 0, + "max": 100 + } + ] + } + ], + "templating": { + "list": [ + { + "name": "server", + "type": "query", + "datasource": "prometheus", + "query": "label_values(server)", + "refresh": 1, + "options": [] + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + }, + "annotations": { + "list": [] + } + } diff --git a/apps/dashboard/pkg/migration/testdata/output/v14.shared_crosshair_to_graph_tooltip.json b/apps/dashboard/pkg/migration/testdata/output/v14.shared_crosshair_to_graph_tooltip.json new file mode 100644 index 00000000000..4367ef0de0c --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v14.shared_crosshair_to_graph_tooltip.json @@ -0,0 +1,99 @@ +{ + "annotations": { + "list": [] + }, + "graphTooltip": 1, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "cpu_usage", + "refId": "A" + } + ], + "title": "CPU Usage Over Time", + "type": "graph", + "yAxes": [ + { + "max": null, + "min": null, + "show": true + } + ] + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "memory_usage", + "refId": "B" + } + ], + "title": "Memory Usage", + "type": "graph", + "yAxes": [ + { + "max": 100, + "min": 0, + "show": true + } + ] + } + ], + "refresh": "", + "schemaVersion": 41, + "templating": { + "list": [ + { + "datasource": { + "uid": "prometheus" + }, + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "title": "V14 Shared Crosshair Migration Test Dashboard" +} \ No newline at end of file diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 623cd10269b..a39818ae099 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -138,7 +138,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1 // indirect + github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0 // indirect github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index a6f06d28df9..d8ee190a9e6 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -673,8 +673,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1 h1:nn19bwMFBGWOvUTxZsdewOfRiXERhKHp0jEira8uQA8= -github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= +github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0 h1:CC+ShIgoLTBsAulRfJH9xF++Us6Y3va3VFbeDeJym2M= +github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 31a9edf0440..fd16b3948da 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -3,7 +3,6 @@ module github.com/grafana/grafana/apps/provisioning go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.40.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.33.3 k8s.io/client-go v0.33.3 @@ -12,48 +11,31 @@ require ( ) require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/getkin/kin-openapi v0.132.0 // indirect github.com/go-logr/logr v1.4.3 // indirect 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/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - 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/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/gomega v1.36.2 // indirect - github.com/perimeterx/marshmallow v1.1.5 // 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.23.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index a560728dc69..c4b1a212f72 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -1,7 +1,3 @@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= @@ -10,8 +6,6 @@ github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtz github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= -github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -22,8 +16,6 @@ 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-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-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= -github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= @@ -36,31 +28,18 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU= -github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk= -github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k= -github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= -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/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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 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.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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -68,33 +47,17 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -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/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/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= @@ -105,16 +68,10 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/constants.go b/apps/provisioning/kinds/provisioning/v0alpha1/constants.go deleted file mode 100644 index c326607a228..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/constants.go +++ /dev/null @@ -1,18 +0,0 @@ -package v0alpha1 - -import "k8s.io/apimachinery/pkg/runtime/schema" - -const ( - // APIGroup is the API group used by all kinds in this package - APIGroup = "provisioning.grafana.app" - // APIVersion is the API version used by all kinds in this package - APIVersion = "v0alpha1" -) - -var ( - // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package - GroupVersion = schema.GroupVersion{ - Group: APIGroup, - Version: APIVersion, - } -) diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_codec_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_codec_gen.go deleted file mode 100644 index 6202f5c6172..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_codec_gen.go +++ /dev/null @@ -1,28 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "encoding/json" - "io" - - "github.com/grafana/grafana-app-sdk/resource" -) - -// RepositoryJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding -type RepositoryJSONCodec struct{} - -// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` -func (*RepositoryJSONCodec) Read(reader io.Reader, into resource.Object) error { - return json.NewDecoder(reader).Decode(into) -} - -// Write writes JSON-encoded bytes into `writer` marshaled from `from` -func (*RepositoryJSONCodec) Write(writer io.Writer, from resource.Object) error { - return json.NewEncoder(writer).Encode(from) -} - -// Interface compliance checks -var _ resource.Codec = &RepositoryJSONCodec{} diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_metadata_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_metadata_gen.go deleted file mode 100644 index 162a34fd13c..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_metadata_gen.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -import ( - time "time" -) - -// metadata contains embedded CommonMetadata and can be extended with custom string fields -// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -// without external reference as using the CommonMetadata reference breaks thema codegen. -type RepositoryMetadata struct { - UpdateTimestamp time.Time `json:"updateTimestamp"` - CreatedBy string `json:"createdBy"` - Uid string `json:"uid"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - ResourceVersion string `json:"resourceVersion"` - Generation int64 `json:"generation"` - UpdatedBy string `json:"updatedBy"` - Labels map[string]string `json:"labels"` -} - -// NewRepositoryMetadata creates a new RepositoryMetadata object. -func NewRepositoryMetadata() *RepositoryMetadata { - return &RepositoryMetadata{ - Finalizers: []string{}, - Labels: map[string]string{}, - } -} diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_object_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_object_gen.go deleted file mode 100644 index 4154ffefb36..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_object_gen.go +++ /dev/null @@ -1,319 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "fmt" - "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "time" -) - -// +k8s:openapi-gen=true -type Repository struct { - metav1.TypeMeta `json:",inline" yaml:",inline"` - metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - - // Spec is the spec of the Repository - Spec RepositorySpec `json:"spec" yaml:"spec"` - - Status RepositoryStatus `json:"status" yaml:"status"` -} - -func (o *Repository) GetSpec() any { - return o.Spec -} - -func (o *Repository) SetSpec(spec any) error { - cast, ok := spec.(RepositorySpec) - if !ok { - return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) - } - o.Spec = cast - return nil -} - -func (o *Repository) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } -} - -func (o *Repository) GetSubresource(name string) (any, bool) { - switch name { - case "status": - return o.Status, true - default: - return nil, false - } -} - -func (o *Repository) SetSubresource(name string, value any) error { - switch name { - case "status": - cast, ok := value.(RepositoryStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type RepositoryStatus", value) - } - o.Status = cast - return nil - default: - return fmt.Errorf("subresource '%s' does not exist", name) - } -} - -func (o *Repository) GetStaticMetadata() resource.StaticMetadata { - gvk := o.GroupVersionKind() - return resource.StaticMetadata{ - Name: o.ObjectMeta.Name, - Namespace: o.ObjectMeta.Namespace, - Group: gvk.Group, - Version: gvk.Version, - Kind: gvk.Kind, - } -} - -func (o *Repository) SetStaticMetadata(metadata resource.StaticMetadata) { - o.Name = metadata.Name - o.Namespace = metadata.Namespace - o.SetGroupVersionKind(schema.GroupVersionKind{ - Group: metadata.Group, - Version: metadata.Version, - Kind: metadata.Kind, - }) -} - -func (o *Repository) GetCommonMetadata() resource.CommonMetadata { - dt := o.DeletionTimestamp - var deletionTimestamp *time.Time - if dt != nil { - deletionTimestamp = &dt.Time - } - // Legacy ExtraFields support - extraFields := make(map[string]any) - if o.Annotations != nil { - extraFields["annotations"] = o.Annotations - } - if o.ManagedFields != nil { - extraFields["managedFields"] = o.ManagedFields - } - if o.OwnerReferences != nil { - extraFields["ownerReferences"] = o.OwnerReferences - } - return resource.CommonMetadata{ - UID: string(o.UID), - ResourceVersion: o.ResourceVersion, - Generation: o.Generation, - Labels: o.Labels, - CreationTimestamp: o.CreationTimestamp.Time, - DeletionTimestamp: deletionTimestamp, - Finalizers: o.Finalizers, - UpdateTimestamp: o.GetUpdateTimestamp(), - CreatedBy: o.GetCreatedBy(), - UpdatedBy: o.GetUpdatedBy(), - ExtraFields: extraFields, - } -} - -func (o *Repository) SetCommonMetadata(metadata resource.CommonMetadata) { - o.UID = types.UID(metadata.UID) - o.ResourceVersion = metadata.ResourceVersion - o.Generation = metadata.Generation - o.Labels = metadata.Labels - o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) - if metadata.DeletionTimestamp != nil { - dt := metav1.NewTime(*metadata.DeletionTimestamp) - o.DeletionTimestamp = &dt - } else { - o.DeletionTimestamp = nil - } - o.Finalizers = metadata.Finalizers - if o.Annotations == nil { - o.Annotations = make(map[string]string) - } - if !metadata.UpdateTimestamp.IsZero() { - o.SetUpdateTimestamp(metadata.UpdateTimestamp) - } - if metadata.CreatedBy != "" { - o.SetCreatedBy(metadata.CreatedBy) - } - if metadata.UpdatedBy != "" { - o.SetUpdatedBy(metadata.UpdatedBy) - } - // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields - if metadata.ExtraFields != nil { - if annotations, ok := metadata.ExtraFields["annotations"]; ok { - if cast, ok := annotations.(map[string]string); ok { - o.Annotations = cast - } - } - if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { - if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { - o.ManagedFields = cast - } - } - if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { - if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { - o.OwnerReferences = cast - } - } - } -} - -func (o *Repository) GetCreatedBy() string { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - return o.ObjectMeta.Annotations["grafana.com/createdBy"] -} - -func (o *Repository) SetCreatedBy(createdBy string) { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy -} - -func (o *Repository) GetUpdateTimestamp() time.Time { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) - return parsed -} - -func (o *Repository) SetUpdateTimestamp(updateTimestamp time.Time) { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) -} - -func (o *Repository) GetUpdatedBy() string { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - return o.ObjectMeta.Annotations["grafana.com/updatedBy"] -} - -func (o *Repository) SetUpdatedBy(updatedBy string) { - if o.ObjectMeta.Annotations == nil { - o.ObjectMeta.Annotations = make(map[string]string) - } - - o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy -} - -func (o *Repository) Copy() resource.Object { - return resource.CopyObject(o) -} - -func (o *Repository) DeepCopyObject() runtime.Object { - return o.Copy() -} - -func (o *Repository) DeepCopy() *Repository { - cpy := &Repository{} - o.DeepCopyInto(cpy) - return cpy -} - -func (o *Repository) DeepCopyInto(dst *Repository) { - dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion - dst.TypeMeta.Kind = o.TypeMeta.Kind - o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) - o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) -} - -// Interface compliance compile-time check -var _ resource.Object = &Repository{} - -// +k8s:openapi-gen=true -type RepositoryList struct { - metav1.TypeMeta `json:",inline" yaml:",inline"` - metav1.ListMeta `json:"metadata" yaml:"metadata"` - Items []Repository `json:"items" yaml:"items"` -} - -func (o *RepositoryList) DeepCopyObject() runtime.Object { - return o.Copy() -} - -func (o *RepositoryList) Copy() resource.ListObject { - cpy := &RepositoryList{ - TypeMeta: o.TypeMeta, - Items: make([]Repository, len(o.Items)), - } - o.ListMeta.DeepCopyInto(&cpy.ListMeta) - for i := 0; i < len(o.Items); i++ { - if item, ok := o.Items[i].Copy().(*Repository); ok { - cpy.Items[i] = *item - } - } - return cpy -} - -func (o *RepositoryList) GetItems() []resource.Object { - items := make([]resource.Object, len(o.Items)) - for i := 0; i < len(o.Items); i++ { - items[i] = &o.Items[i] - } - return items -} - -func (o *RepositoryList) SetItems(items []resource.Object) { - o.Items = make([]Repository, len(items)) - for i := 0; i < len(items); i++ { - o.Items[i] = *items[i].(*Repository) - } -} - -func (o *RepositoryList) DeepCopy() *RepositoryList { - cpy := &RepositoryList{} - o.DeepCopyInto(cpy) - return cpy -} - -func (o *RepositoryList) DeepCopyInto(dst *RepositoryList) { - resource.CopyObjectInto(dst, o) -} - -// Interface compliance compile-time check -var _ resource.ListObject = &RepositoryList{} - -// Copy methods for all subresource types - -// DeepCopy creates a full deep copy of Spec -func (s *RepositorySpec) DeepCopy() *RepositorySpec { - cpy := &RepositorySpec{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies Spec into another Spec object -func (s *RepositorySpec) DeepCopyInto(dst *RepositorySpec) { - resource.CopyObjectInto(dst, s) -} - -// DeepCopy creates a full deep copy of RepositoryStatus -func (s *RepositoryStatus) DeepCopy() *RepositoryStatus { - cpy := &RepositoryStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies RepositoryStatus into another RepositoryStatus object -func (s *RepositoryStatus) DeepCopyInto(dst *RepositoryStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_schema_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_schema_gen.go deleted file mode 100644 index e8f3a525d75..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_schema_gen.go +++ /dev/null @@ -1,34 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "github.com/grafana/grafana-app-sdk/resource" -) - -// schema is unexported to prevent accidental overwrites -var ( - schemaRepository = resource.NewSimpleSchema("provisioning.grafana.app", "v0alpha1", &Repository{}, &RepositoryList{}, resource.WithKind("Repository"), - resource.WithPlural("repositories"), resource.WithScope(resource.NamespacedScope)) - kindRepository = resource.Kind{ - Schema: schemaRepository, - Codecs: map[resource.KindEncoding]resource.Codec{ - resource.KindEncodingJSON: &RepositoryJSONCodec{}, - }, - } -) - -// Kind returns a resource.Kind for this Schema with a JSON codec -func RepositoryKind() resource.Kind { - return kindRepository -} - -// Schema returns a resource.SimpleSchema representation of Repository -func RepositorySchema() *resource.SimpleSchema { - return schemaRepository -} - -// Interface compliance checks -var _ resource.Schema = kindRepository diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_spec_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_spec_gen.go deleted file mode 100644 index 54eb7ba4eda..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_spec_gen.go +++ /dev/null @@ -1,169 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type RepositorySyncOptions struct { - // Enabled must be saved as true before any sync job will run - Enabled bool `json:"enabled"` - // Where values should be saved - Target RepositorySyncOptionsTarget `json:"target"` - // When non-zero, the sync will run periodically - IntervalSeconds *int64 `json:"intervalSeconds,omitempty"` -} - -// NewRepositorySyncOptions creates a new RepositorySyncOptions object. -func NewRepositorySyncOptions() *RepositorySyncOptions { - return &RepositorySyncOptions{} -} - -// +k8s:openapi-gen=true -type RepositoryLocalRepositoryConfig struct { - // Path to the local repository - Path string `json:"path"` -} - -// NewRepositoryLocalRepositoryConfig creates a new RepositoryLocalRepositoryConfig object. -func NewRepositoryLocalRepositoryConfig() *RepositoryLocalRepositoryConfig { - return &RepositoryLocalRepositoryConfig{} -} - -// +k8s:openapi-gen=true -type RepositoryGitHubRepositoryConfig struct { - // The repository URL (e.g. `https://github.com/example/test`). - Url *string `json:"url,omitempty"` - // The branch to use in the repository. - Branch string `json:"branch"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token *string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - EncryptedToken []string `json:"encryptedToken,omitempty"` - // Whether we should show dashboard previews for pull requests. - // By default, this is false (i.e. we will not create previews). - GenerateDashboardPreviews *bool `json:"generateDashboardPreviews,omitempty"` - // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. - Path *string `json:"path,omitempty"` -} - -// NewRepositoryGitHubRepositoryConfig creates a new RepositoryGitHubRepositoryConfig object. -func NewRepositoryGitHubRepositoryConfig() *RepositoryGitHubRepositoryConfig { - return &RepositoryGitHubRepositoryConfig{} -} - -// +k8s:openapi-gen=true -type RepositoryGitRepositoryConfig struct { - // The repository URL (e.g. `https://github.com/example/test.git`). - Url *string `json:"url,omitempty"` - // The branch to use in the repository. - Branch string `json:"branch"` - // TokenUser is the user that will be used to access the repository if it's a personal access token. - TokenUser *string `json:"tokenUser,omitempty"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token *string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - EncryptedToken []string `json:"encryptedToken,omitempty"` - // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. - Path *string `json:"path,omitempty"` -} - -// NewRepositoryGitRepositoryConfig creates a new RepositoryGitRepositoryConfig object. -func NewRepositoryGitRepositoryConfig() *RepositoryGitRepositoryConfig { - return &RepositoryGitRepositoryConfig{} -} - -// +k8s:openapi-gen=true -type RepositoryBitbucketRepositoryConfig struct { - // The repository URL (e.g. `https://bitbucket.org/example/test`). - Url *string `json:"url,omitempty"` - // The branch to use in the repository. - Branch string `json:"branch"` - // TokenUser is the user that will be used to access the repository if it's a personal access token. - TokenUser *string `json:"tokenUser,omitempty"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token *string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - EncryptedToken []string `json:"encryptedToken,omitempty"` - // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. - Path *string `json:"path,omitempty"` -} - -// NewRepositoryBitbucketRepositoryConfig creates a new RepositoryBitbucketRepositoryConfig object. -func NewRepositoryBitbucketRepositoryConfig() *RepositoryBitbucketRepositoryConfig { - return &RepositoryBitbucketRepositoryConfig{} -} - -// +k8s:openapi-gen=true -type RepositoryGitLabRepositoryConfig struct { - // The repository URL (e.g. `https://gitlab.com/example/test`). - Url *string `json:"url,omitempty"` - // The branch to use in the repository. - Branch string `json:"branch"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token *string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - EncryptedToken []string `json:"encryptedToken,omitempty"` - // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. - Path *string `json:"path,omitempty"` -} - -// NewRepositoryGitLabRepositoryConfig creates a new RepositoryGitLabRepositoryConfig object. -func NewRepositoryGitLabRepositoryConfig() *RepositoryGitLabRepositoryConfig { - return &RepositoryGitLabRepositoryConfig{} -} - -// +k8s:openapi-gen=true -type RepositorySpec struct { - // The repository display name (shown in the UI) - Title string `json:"title"` - // Repository description - Description *string `json:"description,omitempty"` - // UI driven Workflow that allow changes to the contends of the repository. - // The order is relevant for defining the precedence of the workflows. - // When empty, the repository does not support any edits (eg, readonly) - Workflows []string `json:"workflows,omitempty"` - // Sync settings -- how values are pulled from the repository into grafana - Sync RepositorySyncOptions `json:"sync"` - // The repository type. When selected oneOf the values below should be non-nil - Type RepositorySpecType `json:"type"` - // The repository on the local file system. - // Mutually exclusive with local | github. - Local *RepositoryLocalRepositoryConfig `json:"local,omitempty"` - // The repository on GitHub. - // Mutually exclusive with local | github | git. - Github *RepositoryGitHubRepositoryConfig `json:"github,omitempty"` - // The repository on Git. - // Mutually exclusive with local | github | git. - Git *RepositoryGitRepositoryConfig `json:"git,omitempty"` - // The repository on Bitbucket. - // Mutually exclusive with local | github | git. - Bitbucket *RepositoryBitbucketRepositoryConfig `json:"bitbucket,omitempty"` - // The repository on GitLab. - // Mutually exclusive with local | github | git. - Gitlab *RepositoryGitLabRepositoryConfig `json:"gitlab,omitempty"` -} - -// NewRepositorySpec creates a new RepositorySpec object. -func NewRepositorySpec() *RepositorySpec { - return &RepositorySpec{ - Sync: *NewRepositorySyncOptions(), - } -} - -// +k8s:openapi-gen=true -type RepositorySyncOptionsTarget string - -const ( - RepositorySyncOptionsTargetUnified RepositorySyncOptionsTarget = "unified" - RepositorySyncOptionsTargetLegacy RepositorySyncOptionsTarget = "legacy" -) - -// +k8s:openapi-gen=true -type RepositorySpecType string - -const ( - RepositorySpecTypeLocal RepositorySpecType = "local" - RepositorySpecTypeGithub RepositorySpecType = "github" - RepositorySpecTypeGit RepositorySpecType = "git" - RepositorySpecTypeBitbucket RepositorySpecType = "bitbucket" - RepositorySpecTypeGitlab RepositorySpecType = "gitlab" -) diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/repository_status_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/repository_status_gen.go deleted file mode 100644 index c2512cc2851..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/repository_status_gen.go +++ /dev/null @@ -1,137 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type RepositoryHealthStatus struct { - // When not healthy, requests will not be executed - Healthy bool `json:"healthy"` - // When the health was checked last time - Checked *int64 `json:"checked,omitempty"` - // Summary messages (can be shown to users) - // Will only be populated when not healthy - Message []string `json:"message,omitempty"` -} - -// NewRepositoryHealthStatus creates a new RepositoryHealthStatus object. -func NewRepositoryHealthStatus() *RepositoryHealthStatus { - return &RepositoryHealthStatus{} -} - -// +k8s:openapi-gen=true -type RepositorySyncStatus struct { - // pending, running, success, error - State RepositorySyncStatusState `json:"state"` - // The ID for the job that ran this sync - Job *string `json:"job,omitempty"` - // When the sync job started - Started *int64 `json:"started,omitempty"` - // When the sync job finished - Finished *int64 `json:"finished,omitempty"` - // When the next sync check is scheduled - Scheduled *int64 `json:"scheduled,omitempty"` - // Summary messages (will be shown to users) - Message []string `json:"message"` - // The repository ref when the last successful sync ran - LastRef *string `json:"lastRef,omitempty"` - // Incremental synchronization for versioned repositories - Incremental *bool `json:"incremental,omitempty"` -} - -// NewRepositorySyncStatus creates a new RepositorySyncStatus object. -func NewRepositorySyncStatus() *RepositorySyncStatus { - return &RepositorySyncStatus{ - Message: []string{}, - } -} - -// +k8s:openapi-gen=true -type RepositoryResourceCount struct { - Group string `json:"group"` - Resource string `json:"resource"` - Count int64 `json:"count"` -} - -// NewRepositoryResourceCount creates a new RepositoryResourceCount object. -func NewRepositoryResourceCount() *RepositoryResourceCount { - return &RepositoryResourceCount{} -} - -// +k8s:openapi-gen=true -type RepositorystatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State RepositoryStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewRepositorystatusOperatorState creates a new RepositorystatusOperatorState object. -func NewRepositorystatusOperatorState() *RepositorystatusOperatorState { - return &RepositorystatusOperatorState{} -} - -// +k8s:openapi-gen=true -type RepositoryWebhookStatus struct { - Id *int64 `json:"id,omitempty"` - Url *string `json:"url,omitempty"` - Secret *string `json:"secret,omitempty"` - EncryptedSecret []string `json:"encryptedSecret,omitempty"` - SubscribedEvents []string `json:"subscribedEvents,omitempty"` - LastEvent *int64 `json:"lastEvent,omitempty"` -} - -// NewRepositoryWebhookStatus creates a new RepositoryWebhookStatus object. -func NewRepositoryWebhookStatus() *RepositoryWebhookStatus { - return &RepositoryWebhookStatus{} -} - -// +k8s:openapi-gen=true -type RepositoryStatus struct { - // The generation of the spec last time reconciliation ran - ObservedGeneration *int64 `json:"observedGeneration,omitempty"` - // This will get updated with the current health status (and updated periodically) - Health RepositoryHealthStatus `json:"health"` - // Sync information with the last sync information - Sync RepositorySyncStatus `json:"sync"` - // The object count when sync last ran - Stats []RepositoryResourceCount `json:"stats,omitempty"` - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]RepositorystatusOperatorState `json:"operatorStates,omitempty"` - // Webhook Information (if applicable) - Webhook *RepositoryWebhookStatus `json:"webhook,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewRepositoryStatus creates a new RepositoryStatus object. -func NewRepositoryStatus() *RepositoryStatus { - return &RepositoryStatus{ - Health: *NewRepositoryHealthStatus(), - Sync: *NewRepositorySyncStatus(), - } -} - -// +k8s:openapi-gen=true -type RepositorySyncStatusState string - -const ( - RepositorySyncStatusStatePending RepositorySyncStatusState = "pending" - RepositorySyncStatusStateRunning RepositorySyncStatusState = "running" - RepositorySyncStatusStateSuccess RepositorySyncStatusState = "success" - RepositorySyncStatusStateError RepositorySyncStatusState = "error" -) - -// +k8s:openapi-gen=true -type RepositoryStatusOperatorStateState string - -const ( - RepositoryStatusOperatorStateStateSuccess RepositoryStatusOperatorStateState = "success" - RepositoryStatusOperatorStateStateInProgress RepositoryStatusOperatorStateState = "in_progress" - RepositoryStatusOperatorStateStateFailed RepositoryStatusOperatorStateState = "failed" -) diff --git a/apps/provisioning/kinds/provisioning/v0alpha1/zz_openapi_gen.go b/apps/provisioning/kinds/provisioning/v0alpha1/zz_openapi_gen.go deleted file mode 100644 index 583d9f07f60..00000000000 --- a/apps/provisioning/kinds/provisioning/v0alpha1/zz_openapi_gen.go +++ /dev/null @@ -1,879 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Code generated by grafana-app-sdk. DO NOT EDIT. - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.Repository": schema_provisioning_kinds_provisioning_v0alpha1_Repository(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryBitbucketRepositoryConfig": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryBitbucketRepositoryConfig(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitHubRepositoryConfig": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitHubRepositoryConfig(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitLabRepositoryConfig": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitLabRepositoryConfig(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitRepositoryConfig": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitRepositoryConfig(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryHealthStatus": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryHealthStatus(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryList": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryList(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryLocalRepositoryConfig": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryLocalRepositoryConfig(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryResourceCount": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryResourceCount(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySpec": schema_provisioning_kinds_provisioning_v0alpha1_RepositorySpec(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryStatus": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryStatus(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncOptions": schema_provisioning_kinds_provisioning_v0alpha1_RepositorySyncOptions(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncStatus": schema_provisioning_kinds_provisioning_v0alpha1_RepositorySyncStatus(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryWebhookStatus": schema_provisioning_kinds_provisioning_v0alpha1_RepositoryWebhookStatus(ref), - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorystatusOperatorState": schema_provisioning_kinds_provisioning_v0alpha1_RepositorystatusOperatorState(ref), - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_Repository(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.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Spec is the spec of the Repository", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryStatus"), - }, - }, - }, - Required: []string{"metadata", "spec", "status"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySpec", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryBitbucketRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "url": { - SchemaProps: spec.SchemaProps{ - Description: "The repository URL (e.g. `https://bitbucket.org/example/test`).", - Type: []string{"string"}, - Format: "", - }, - }, - "branch": { - SchemaProps: spec.SchemaProps{ - Description: "The branch to use in the repository.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "tokenUser": { - SchemaProps: spec.SchemaProps{ - Description: "TokenUser is the user that will be used to access the repository if it's a personal access token.", - Type: []string{"string"}, - Format: "", - }, - }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"branch"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitHubRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "url": { - SchemaProps: spec.SchemaProps{ - Description: "The repository URL (e.g. `https://github.com/example/test`).", - Type: []string{"string"}, - Format: "", - }, - }, - "branch": { - SchemaProps: spec.SchemaProps{ - Description: "The branch to use in the repository.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "generateDashboardPreviews": { - SchemaProps: spec.SchemaProps{ - Description: "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).", - Type: []string{"boolean"}, - Format: "", - }, - }, - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"branch"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitLabRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "url": { - SchemaProps: spec.SchemaProps{ - Description: "The repository URL (e.g. `https://gitlab.com/example/test`).", - Type: []string{"string"}, - Format: "", - }, - }, - "branch": { - SchemaProps: spec.SchemaProps{ - Description: "The branch to use in the repository.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"branch"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryGitRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "url": { - SchemaProps: spec.SchemaProps{ - Description: "The repository URL (e.g. `https://github.com/example/test.git`).", - Type: []string{"string"}, - Format: "", - }, - }, - "branch": { - SchemaProps: spec.SchemaProps{ - Description: "The branch to use in the repository.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "tokenUser": { - SchemaProps: spec.SchemaProps{ - Description: "TokenUser is the user that will be used to access the repository if it's a personal access token.", - Type: []string{"string"}, - Format: "", - }, - }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"branch"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryHealthStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "healthy": { - SchemaProps: spec.SchemaProps{ - Description: "When not healthy, requests will not be executed", - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "checked": { - SchemaProps: spec.SchemaProps{ - Description: "When the health was checked last time", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "message": { - SchemaProps: spec.SchemaProps{ - Description: "Summary messages (can be shown to users) Will only be populated when not healthy", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"healthy"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryList(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/apps/provisioning/kinds/provisioning/v0alpha1.Repository"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.Repository", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryLocalRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path to the local repository", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"path"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryResourceCount(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "group": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "resource": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "count": { - SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - Required: []string{"group", "resource", "count"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Description: "The repository display name (shown in the UI)", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "description": { - SchemaProps: spec.SchemaProps{ - Description: "Repository description", - Type: []string{"string"}, - Format: "", - }, - }, - "workflows": { - SchemaProps: spec.SchemaProps{ - Description: "UI driven Workflow that allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly)", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "sync": { - SchemaProps: spec.SchemaProps{ - Description: "Sync settings -- how values are pulled from the repository into grafana", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncOptions"), - }, - }, - "type": { - SchemaProps: spec.SchemaProps{ - Description: "The repository type. When selected oneOf the values below should be non-nil", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "local": { - SchemaProps: spec.SchemaProps{ - Description: "The repository on the local file system. Mutually exclusive with local | github.", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryLocalRepositoryConfig"), - }, - }, - "github": { - SchemaProps: spec.SchemaProps{ - Description: "The repository on GitHub. Mutually exclusive with local | github | git.", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitHubRepositoryConfig"), - }, - }, - "git": { - SchemaProps: spec.SchemaProps{ - Description: "The repository on Git. Mutually exclusive with local | github | git.", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitRepositoryConfig"), - }, - }, - "bitbucket": { - SchemaProps: spec.SchemaProps{ - Description: "The repository on Bitbucket. Mutually exclusive with local | github | git.", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryBitbucketRepositoryConfig"), - }, - }, - "gitlab": { - SchemaProps: spec.SchemaProps{ - Description: "The repository on GitLab. Mutually exclusive with local | github | git.", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitLabRepositoryConfig"), - }, - }, - }, - Required: []string{"title", "sync", "type"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryBitbucketRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitHubRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitLabRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryGitRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryLocalRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncOptions"}, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "observedGeneration": { - SchemaProps: spec.SchemaProps{ - Description: "The generation of the spec last time reconciliation ran", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "health": { - SchemaProps: spec.SchemaProps{ - Description: "This will get updated with the current health status (and updated periodically)", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryHealthStatus"), - }, - }, - "sync": { - SchemaProps: spec.SchemaProps{ - Description: "Sync information with the last sync information", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncStatus"), - }, - }, - "stats": { - SchemaProps: spec.SchemaProps{ - Description: "The object count when sync last ran", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryResourceCount"), - }, - }, - }, - }, - }, - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorystatusOperatorState"), - }, - }, - }, - }, - }, - "webhook": { - SchemaProps: spec.SchemaProps{ - Description: "Webhook Information (if applicable)", - Ref: ref("github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryWebhookStatus"), - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"health", "sync"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryHealthStatus", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryResourceCount", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorySyncStatus", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositoryWebhookStatus", "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1.RepositorystatusOperatorState"}, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositorySyncOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "enabled": { - SchemaProps: spec.SchemaProps{ - Description: "Enabled must be saved as true before any sync job will run", - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "target": { - SchemaProps: spec.SchemaProps{ - Description: "Where values should be saved", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "intervalSeconds": { - SchemaProps: spec.SchemaProps{ - Description: "When non-zero, the sync will run periodically", - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - Required: []string{"enabled", "target"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositorySyncStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "state": { - SchemaProps: spec.SchemaProps{ - Description: "pending, running, success, error", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "job": { - SchemaProps: spec.SchemaProps{ - Description: "The ID for the job that ran this sync", - Type: []string{"string"}, - Format: "", - }, - }, - "started": { - SchemaProps: spec.SchemaProps{ - Description: "When the sync job started", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "finished": { - SchemaProps: spec.SchemaProps{ - Description: "When the sync job finished", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "scheduled": { - SchemaProps: spec.SchemaProps{ - Description: "When the next sync check is scheduled", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "message": { - SchemaProps: spec.SchemaProps{ - Description: "Summary messages (will be shown to users)", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "lastRef": { - SchemaProps: spec.SchemaProps{ - Description: "The repository ref when the last successful sync ran", - Type: []string{"string"}, - Format: "", - }, - }, - "incremental": { - SchemaProps: spec.SchemaProps{ - Description: "Incremental synchronization for versioned repositories", - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - Required: []string{"state", "message"}, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositoryWebhookStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "id": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - "url": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "secret": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedSecret": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "subscribedEvents": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "lastEvent": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - }, - }, - } -} - -func schema_provisioning_kinds_provisioning_v0alpha1_RepositorystatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} diff --git a/apps/provisioning/kinds/provisioning_manifest.go b/apps/provisioning/kinds/provisioning_manifest.go deleted file mode 100644 index 677a17d8b55..00000000000 --- a/apps/provisioning/kinds/provisioning_manifest.go +++ /dev/null @@ -1,83 +0,0 @@ -// -// This file is generated by grafana-app-sdk -// DO NOT EDIT -// - -package kinds - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/grafana/grafana-app-sdk/app" - "github.com/grafana/grafana-app-sdk/resource" - - v0alpha1 "github.com/grafana/grafana/apps/provisioning/kinds/provisioning/v0alpha1" -) - -var ( - rawSchemaRepositoryv0alpha1 = []byte(`{"spec":{"properties":{"bitbucket":{"description":"The repository on Bitbucket.\nMutually exclusive with local | github | git.","properties":{"branch":{"description":"The branch to use in the repository.","type":"string"},"encryptedToken":{"description":"Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.","items":{"type":"string"},"type":"array"},"path":{"description":"Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.","type":"string"},"token":{"description":"Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.","type":"string"},"tokenUser":{"description":"TokenUser is the user that will be used to access the repository if it's a personal access token.","type":"string"},"url":{"description":"The repository URL (e.g. ` + "`" + `https://bitbucket.org/example/test` + "`" + `).","type":"string"}},"required":["branch"],"type":"object"},"description":{"description":"Repository description","type":"string"},"git":{"description":"The repository on Git.\nMutually exclusive with local | github | git.","properties":{"branch":{"description":"The branch to use in the repository.","type":"string"},"encryptedToken":{"description":"Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.","items":{"type":"string"},"type":"array"},"path":{"description":"Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.","type":"string"},"token":{"description":"Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.","type":"string"},"tokenUser":{"description":"TokenUser is the user that will be used to access the repository if it's a personal access token.","type":"string"},"url":{"description":"The repository URL (e.g. ` + "`" + `https://github.com/example/test.git` + "`" + `).","type":"string"}},"required":["branch"],"type":"object"},"github":{"description":"The repository on GitHub.\nMutually exclusive with local | github | git.","properties":{"branch":{"description":"The branch to use in the repository.","type":"string"},"encryptedToken":{"description":"Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.","items":{"type":"string"},"type":"array"},"generateDashboardPreviews":{"description":"Whether we should show dashboard previews for pull requests.\nBy default, this is false (i.e. we will not create previews).","type":"boolean"},"path":{"description":"Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.","type":"string"},"token":{"description":"Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.","type":"string"},"url":{"description":"The repository URL (e.g. ` + "`" + `https://github.com/example/test` + "`" + `).","type":"string"}},"required":["branch"],"type":"object"},"gitlab":{"description":"The repository on GitLab.\nMutually exclusive with local | github | git.","properties":{"branch":{"description":"The branch to use in the repository.","type":"string"},"encryptedToken":{"description":"Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.","items":{"type":"string"},"type":"array"},"path":{"description":"Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository.","type":"string"},"token":{"description":"Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.","type":"string"},"url":{"description":"The repository URL (e.g. ` + "`" + `https://gitlab.com/example/test` + "`" + `).","type":"string"}},"required":["branch"],"type":"object"},"local":{"description":"The repository on the local file system.\nMutually exclusive with local | github.","properties":{"path":{"description":"Path to the local repository","type":"string"}},"required":["path"],"type":"object"},"sync":{"description":"Sync settings -- how values are pulled from the repository into grafana","properties":{"enabled":{"description":"Enabled must be saved as true before any sync job will run","type":"boolean"},"intervalSeconds":{"description":"When non-zero, the sync will run periodically","type":"integer"},"target":{"description":"Where values should be saved","enum":["unified","legacy"],"type":"string"}},"required":["enabled","target"],"type":"object"},"title":{"description":"The repository display name (shown in the UI)","type":"string"},"type":{"description":"The repository type. When selected oneOf the values below should be non-nil","enum":["local","github","git","bitbucket","gitlab"],"type":"string"},"workflows":{"description":"UI driven Workflow that allow changes to the contends of the repository.\nThe order is relevant for defining the precedence of the workflows.\nWhen empty, the repository does not support any edits (eg, readonly)","items":{"type":"string"},"type":"array"}},"required":["title","sync","type"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"health":{"description":"This will get updated with the current health status (and updated periodically)","properties":{"checked":{"description":"When the health was checked last time","type":"integer"},"healthy":{"description":"When not healthy, requests will not be executed","type":"boolean"},"message":{"description":"Summary messages (can be shown to users)\nWill only be populated when not healthy","items":{"type":"string"},"type":"array"}},"required":["healthy"],"type":"object"},"observedGeneration":{"description":"The generation of the spec last time reconciliation ran","type":"integer"},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"stats":{"description":"The object count when sync last ran","items":{"properties":{"count":{"type":"integer"},"group":{"type":"string"},"resource":{"type":"string"}},"required":["group","resource","count"],"type":"object"},"type":"array"},"sync":{"description":"Sync information with the last sync information","properties":{"finished":{"description":"When the sync job finished","type":"integer"},"incremental":{"description":"Incremental synchronization for versioned repositories","type":"boolean"},"job":{"description":"The ID for the job that ran this sync","type":"string"},"lastRef":{"description":"The repository ref when the last successful sync ran","type":"string"},"message":{"description":"Summary messages (will be shown to users)","items":{"type":"string"},"type":"array"},"scheduled":{"description":"When the next sync check is scheduled","type":"integer"},"started":{"description":"When the sync job started","type":"integer"},"state":{"description":"pending, running, success, error","enum":["pending","running","success","error"],"type":"string"}},"required":["state","message"],"type":"object"},"webhook":{"description":"Webhook Information (if applicable)","properties":{"encryptedSecret":{"items":{"type":"string"},"type":"array"},"id":{"type":"integer"},"lastEvent":{"type":"integer"},"secret":{"type":"string"},"subscribedEvents":{"items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"}},"required":["health","sync"],"type":"object"}}`) - versionSchemaRepositoryv0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaRepositoryv0alpha1, &versionSchemaRepositoryv0alpha1) -) - -var appManifestData = app.ManifestData{ - AppName: "provisioning", - Group: "provisioning.grafana.app", - Versions: []app.ManifestVersion{ - { - Name: "v0alpha1", - Served: true, - Kinds: []app.ManifestVersionKind{ - { - Kind: "Repository", - Plural: "Repositories", - Scope: "Namespaced", - Conversion: false, - Admission: &app.AdmissionCapabilities{ - Validation: &app.ValidationCapability{ - Operations: []app.AdmissionOperation{ - app.AdmissionOperationCreate, - app.AdmissionOperationUpdate, - }, - }, - }, - Schema: &versionSchemaRepositoryv0alpha1, - }, - }, - }, - }, -} - -func LocalManifest() app.Manifest { - return app.NewEmbeddedManifest(appManifestData) -} - -func RemoteManifest() app.Manifest { - return app.NewAPIServerManifest("provisioning") -} - -var kindVersionToGoType = map[string]resource.Kind{ - "Repository/v0alpha1": v0alpha1.RepositoryKind(), -} - -// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. -// If there is no association for the provided Kind and Version, exists will return false. -func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { - goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] - return goType, exists -} - -var customRouteToGoResponseType = map[string]any{} - -// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. -// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. -// If there is no association for the provided kind, version, custom route path, and method, exists will return false. -func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { - if len(path) > 0 && path[0] == '/' { - path = path[1:] - } - goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] - return goType, exists -} diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go index bc673959164..7ee2bcb4c95 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go @@ -21,12 +21,11 @@ type Repository struct { Status RepositoryStatus `json:"status,omitempty"` } -// NOT YET USED FOR REAL -- testing secure value workflow type SecureValues struct { // Token used to connect the configured repository Token common.InlineSecureValue `json:"token,omitzero,omitempty"` - // Some webhooks (github) require a secret key value + // Some webhooks (including github) require a secret key value WebhookSecret common.InlineSecureValue `json:"webhookSecret,omitzero,omitempty"` } @@ -55,11 +54,6 @@ type GitHubRepositoryConfig struct { // The branch to use in the repository. Branch string `json:"branch"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - // +listType=atomic - EncryptedToken []byte `json:"encryptedToken,omitempty"` // Whether we should show dashboard previews for pull requests. // By default, this is false (i.e. we will not create previews). @@ -80,11 +74,6 @@ type GitRepositoryConfig struct { Branch string `json:"branch"` // TokenUser is the user that will be used to access the repository if it's a personal access token. TokenUser string `json:"tokenUser,omitempty"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - // +listType=atomic - EncryptedToken []byte `json:"encryptedToken,omitempty"` // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. // This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. // The path is relative to the root of the repository, regardless of the leading slash. @@ -100,11 +89,6 @@ type BitbucketRepositoryConfig struct { Branch string `json:"branch"` // TokenUser is the user that will be used to access the repository if it's a personal access token. TokenUser string `json:"tokenUser,omitempty"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - // +listType=atomic - EncryptedToken []byte `json:"encryptedToken,omitempty"` // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. // This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. // The path is relative to the root of the repository, regardless of the leading slash. @@ -118,11 +102,6 @@ type GitLabRepositoryConfig struct { URL string `json:"url,omitempty"` // The branch to use in the repository. Branch string `json:"branch"` - // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. - Token string `json:"token,omitempty"` - // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. - // +listType=atomic - EncryptedToken []byte `json:"encryptedToken,omitempty"` // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. // This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. // The path is relative to the root of the repository, regardless of the leading slash. @@ -297,8 +276,6 @@ type SyncStatus struct { type WebhookStatus struct { ID int64 `json:"id,omitempty"` URL string `json:"url,omitempty"` - Secret string `json:"secret,omitempty"` - EncryptedSecret []byte `json:"encryptedSecret,omitempty"` SubscribedEvents []string `json:"subscribedEvents,omitempty"` LastEvent int64 `json:"lastEvent,omitempty"` } diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 5e09384fc9f..24d3b769f64 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -30,11 +30,6 @@ func (in *Author) DeepCopy() *Author { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BitbucketRepositoryConfig) DeepCopyInto(out *BitbucketRepositoryConfig) { *out = *in - if in.EncryptedToken != nil { - in, out := &in.EncryptedToken, &out.EncryptedToken - *out = make([]byte, len(*in)) - copy(*out, *in) - } return } @@ -156,11 +151,6 @@ func (in *FileList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubRepositoryConfig) DeepCopyInto(out *GitHubRepositoryConfig) { *out = *in - if in.EncryptedToken != nil { - in, out := &in.EncryptedToken, &out.EncryptedToken - *out = make([]byte, len(*in)) - copy(*out, *in) - } return } @@ -177,11 +167,6 @@ func (in *GitHubRepositoryConfig) DeepCopy() *GitHubRepositoryConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitLabRepositoryConfig) DeepCopyInto(out *GitLabRepositoryConfig) { *out = *in - if in.EncryptedToken != nil { - in, out := &in.EncryptedToken, &out.EncryptedToken - *out = make([]byte, len(*in)) - copy(*out, *in) - } return } @@ -198,11 +183,6 @@ func (in *GitLabRepositoryConfig) DeepCopy() *GitLabRepositoryConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRepositoryConfig) DeepCopyInto(out *GitRepositoryConfig) { *out = *in - if in.EncryptedToken != nil { - in, out := &in.EncryptedToken, &out.EncryptedToken - *out = make([]byte, len(*in)) - copy(*out, *in) - } return } @@ -738,22 +718,22 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { if in.GitHub != nil { in, out := &in.GitHub, &out.GitHub *out = new(GitHubRepositoryConfig) - (*in).DeepCopyInto(*out) + **out = **in } if in.Git != nil { in, out := &in.Git, &out.Git *out = new(GitRepositoryConfig) - (*in).DeepCopyInto(*out) + **out = **in } if in.Bitbucket != nil { in, out := &in.Bitbucket, &out.Bitbucket *out = new(BitbucketRepositoryConfig) - (*in).DeepCopyInto(*out) + **out = **in } if in.GitLab != nil { in, out := &in.GitLab, &out.GitLab *out = new(GitLabRepositoryConfig) - (*in).DeepCopyInto(*out) + **out = **in } return } @@ -1220,11 +1200,6 @@ func (in *WebhookResponse) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WebhookStatus) DeepCopyInto(out *WebhookStatus) { *out = *in - if in.EncryptedSecret != nil { - in, out := &in.EncryptedSecret, &out.EncryptedSecret - *out = make([]byte, len(*in)) - copy(*out, *in) - } if in.SubscribedEvents != nil { in, out := &in.SubscribedEvents, &out.SubscribedEvents *out = make([]string, len(*in)) diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 99244e9d5fc..9b3a795009f 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -128,25 +128,6 @@ func schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref common. Format: "", }, }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"string"}, - Format: "byte", - }, - }, "path": { SchemaProps: spec.SchemaProps{ Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", @@ -402,25 +383,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref Format: "", }, }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"string"}, - Format: "byte", - }, - }, "generateDashboardPreviews": { SchemaProps: spec.SchemaProps{ Description: "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).", @@ -463,25 +425,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitLabRepositoryConfig(ref common.Ref Format: "", }, }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"string"}, - Format: "byte", - }, - }, "path": { SchemaProps: spec.SchemaProps{ Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", @@ -524,25 +467,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref common.Refere Format: "", }, }, - "token": { - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedToken": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - Type: []string{"string"}, - Format: "byte", - }, - }, "path": { SchemaProps: spec.SchemaProps{ Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", @@ -2378,8 +2302,7 @@ func schema_pkg_apis_provisioning_v0alpha1_SecureValues(ref common.ReferenceCall return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "NOT YET USED FOR REAL -- testing secure value workflow", - Type: []string{"object"}, + Type: []string{"object"}, Properties: map[string]spec.Schema{ "token": { SchemaProps: spec.SchemaProps{ @@ -2390,7 +2313,7 @@ func schema_pkg_apis_provisioning_v0alpha1_SecureValues(ref common.ReferenceCall }, "webhookSecret": { SchemaProps: spec.SchemaProps{ - Description: "Some webhooks (github) require a secret key value", + Description: "Some webhooks (including github) require a secret key value", Default: map[string]interface{}{}, Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"), }, @@ -2671,18 +2594,6 @@ func schema_pkg_apis_provisioning_v0alpha1_WebhookStatus(ref common.ReferenceCal Format: "", }, }, - "secret": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "encryptedSecret": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "byte", - }, - }, "subscribedEvents": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go index 6a81bf1625b..da3427a4b81 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go @@ -7,12 +7,10 @@ package v0alpha1 // BitbucketRepositoryConfigApplyConfiguration represents a declarative configuration of the BitbucketRepositoryConfig type for use // with apply. type BitbucketRepositoryConfigApplyConfiguration struct { - URL *string `json:"url,omitempty"` - Branch *string `json:"branch,omitempty"` - TokenUser *string `json:"tokenUser,omitempty"` - Token *string `json:"token,omitempty"` - EncryptedToken []byte `json:"encryptedToken,omitempty"` - Path *string `json:"path,omitempty"` + URL *string `json:"url,omitempty"` + Branch *string `json:"branch,omitempty"` + TokenUser *string `json:"tokenUser,omitempty"` + Path *string `json:"path,omitempty"` } // BitbucketRepositoryConfigApplyConfiguration constructs a declarative configuration of the BitbucketRepositoryConfig type for use with @@ -45,24 +43,6 @@ func (b *BitbucketRepositoryConfigApplyConfiguration) WithTokenUser(value string return b } -// WithToken sets the Token field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Token field is set to the value of the last call. -func (b *BitbucketRepositoryConfigApplyConfiguration) WithToken(value string) *BitbucketRepositoryConfigApplyConfiguration { - b.Token = &value - return b -} - -// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the EncryptedToken field. -func (b *BitbucketRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *BitbucketRepositoryConfigApplyConfiguration { - for i := range values { - b.EncryptedToken = append(b.EncryptedToken, values[i]) - } - return b -} - // WithPath sets the Path field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Path field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go index 89b2b9893a8..db412252371 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go @@ -9,8 +9,6 @@ package v0alpha1 type GitHubRepositoryConfigApplyConfiguration struct { URL *string `json:"url,omitempty"` Branch *string `json:"branch,omitempty"` - Token *string `json:"token,omitempty"` - EncryptedToken []byte `json:"encryptedToken,omitempty"` GenerateDashboardPreviews *bool `json:"generateDashboardPreviews,omitempty"` Path *string `json:"path,omitempty"` } @@ -37,24 +35,6 @@ func (b *GitHubRepositoryConfigApplyConfiguration) WithBranch(value string) *Git return b } -// WithToken sets the Token field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Token field is set to the value of the last call. -func (b *GitHubRepositoryConfigApplyConfiguration) WithToken(value string) *GitHubRepositoryConfigApplyConfiguration { - b.Token = &value - return b -} - -// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the EncryptedToken field. -func (b *GitHubRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitHubRepositoryConfigApplyConfiguration { - for i := range values { - b.EncryptedToken = append(b.EncryptedToken, values[i]) - } - return b -} - // WithGenerateDashboardPreviews sets the GenerateDashboardPreviews field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateDashboardPreviews field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go index dfa6d7f9620..1b133cf7bd9 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go @@ -7,11 +7,9 @@ package v0alpha1 // GitLabRepositoryConfigApplyConfiguration represents a declarative configuration of the GitLabRepositoryConfig type for use // with apply. type GitLabRepositoryConfigApplyConfiguration struct { - URL *string `json:"url,omitempty"` - Branch *string `json:"branch,omitempty"` - Token *string `json:"token,omitempty"` - EncryptedToken []byte `json:"encryptedToken,omitempty"` - Path *string `json:"path,omitempty"` + URL *string `json:"url,omitempty"` + Branch *string `json:"branch,omitempty"` + Path *string `json:"path,omitempty"` } // GitLabRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitLabRepositoryConfig type for use with @@ -36,24 +34,6 @@ func (b *GitLabRepositoryConfigApplyConfiguration) WithBranch(value string) *Git return b } -// WithToken sets the Token field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Token field is set to the value of the last call. -func (b *GitLabRepositoryConfigApplyConfiguration) WithToken(value string) *GitLabRepositoryConfigApplyConfiguration { - b.Token = &value - return b -} - -// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the EncryptedToken field. -func (b *GitLabRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitLabRepositoryConfigApplyConfiguration { - for i := range values { - b.EncryptedToken = append(b.EncryptedToken, values[i]) - } - return b -} - // WithPath sets the Path field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Path field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitrepositoryconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitrepositoryconfig.go index 8725c3a4fcd..51c379b5fc9 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitrepositoryconfig.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitrepositoryconfig.go @@ -7,12 +7,10 @@ package v0alpha1 // GitRepositoryConfigApplyConfiguration represents a declarative configuration of the GitRepositoryConfig type for use // with apply. type GitRepositoryConfigApplyConfiguration struct { - URL *string `json:"url,omitempty"` - Branch *string `json:"branch,omitempty"` - TokenUser *string `json:"tokenUser,omitempty"` - Token *string `json:"token,omitempty"` - EncryptedToken []byte `json:"encryptedToken,omitempty"` - Path *string `json:"path,omitempty"` + URL *string `json:"url,omitempty"` + Branch *string `json:"branch,omitempty"` + TokenUser *string `json:"tokenUser,omitempty"` + Path *string `json:"path,omitempty"` } // GitRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitRepositoryConfig type for use with @@ -45,24 +43,6 @@ func (b *GitRepositoryConfigApplyConfiguration) WithTokenUser(value string) *Git return b } -// WithToken sets the Token field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Token field is set to the value of the last call. -func (b *GitRepositoryConfigApplyConfiguration) WithToken(value string) *GitRepositoryConfigApplyConfiguration { - b.Token = &value - return b -} - -// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the EncryptedToken field. -func (b *GitRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitRepositoryConfigApplyConfiguration { - for i := range values { - b.EncryptedToken = append(b.EncryptedToken, values[i]) - } - return b -} - // WithPath sets the Path field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Path field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/webhookstatus.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/webhookstatus.go index 23eed891b36..9d1aabead87 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/webhookstatus.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/webhookstatus.go @@ -9,8 +9,6 @@ package v0alpha1 type WebhookStatusApplyConfiguration struct { ID *int64 `json:"id,omitempty"` URL *string `json:"url,omitempty"` - Secret *string `json:"secret,omitempty"` - EncryptedSecret []byte `json:"encryptedSecret,omitempty"` SubscribedEvents []string `json:"subscribedEvents,omitempty"` LastEvent *int64 `json:"lastEvent,omitempty"` } @@ -37,24 +35,6 @@ func (b *WebhookStatusApplyConfiguration) WithURL(value string) *WebhookStatusAp return b } -// WithSecret sets the Secret field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Secret field is set to the value of the last call. -func (b *WebhookStatusApplyConfiguration) WithSecret(value string) *WebhookStatusApplyConfiguration { - b.Secret = &value - return b -} - -// WithEncryptedSecret adds the given value to the EncryptedSecret field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the EncryptedSecret field. -func (b *WebhookStatusApplyConfiguration) WithEncryptedSecret(values ...byte) *WebhookStatusApplyConfiguration { - for i := range values { - b.EncryptedSecret = append(b.EncryptedSecret, values[i]) - } - return b -} - // WithSubscribedEvents adds the given value to the SubscribedEvents field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the SubscribedEvents field. diff --git a/apps/secret/kinds/v1beta1/securevalue.cue b/apps/secret/kinds/v1beta1/securevalue.cue index 9fef8e63f46..6305bb563ff 100644 --- a/apps/secret/kinds/v1beta1/securevalue.cue +++ b/apps/secret/kinds/v1beta1/securevalue.cue @@ -19,7 +19,7 @@ SecureValueSpec: { // Minimum and maximum lengths in bytes. // +k8s:validation:minLength=1 // +k8s:validation:maxLength=24576 - // +optional + // +optional value?: #ExposedSecureValue // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. @@ -41,13 +41,16 @@ SecureValueSpec: { // +k8s:validation:maxItems=64 // +k8s:validation:uniqueItems=true // +listType=atomic - // +optional + // +optional decrypters?: [...string] & list.UniqueItems() & list.MaxItems(64) } SecureValueStatus: { + // Version of the secure value. Cannot be set. + // +optional version: int64 & >=0 + // External ID where the secret is stored. Cannot be set. // +optional externalID: string } diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go index eac159c6035..a2534ab464d 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go @@ -22,10 +22,13 @@ func NewSecureValuestatusOperatorState() *SecureValuestatusOperatorState { // +k8s:openapi-gen=true type SecureValueStatus struct { + // Version of the secure value. Cannot be set. + // +optional Version int64 `json:"version"` // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"` + // External ID where the secret is stored. Cannot be set. // +optional ExternalID string `json:"externalID"` // additionalFields is reserved for future use diff --git a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go index 87ba7504932..7a3b6d090c0 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go @@ -633,9 +633,10 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba Properties: map[string]spec.Schema{ "version": { SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"integer"}, - Format: "int64", + Description: "Version of the secure value. Cannot be set.", + Default: 0, + Type: []string{"integer"}, + Format: "int64", }, }, "operatorStates": { @@ -655,9 +656,10 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba }, "externalID": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "External ID where the secret is stored. Cannot be set.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "additionalFields": { @@ -676,7 +678,6 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba }, }, }, - Required: []string{"version"}, }, }, Dependencies: []string{ diff --git a/contribute/backend/services.md b/contribute/backend/services.md index 0bf6aa00cfe..e6dcbe038d6 100644 --- a/contribute/backend/services.md +++ b/contribute/backend/services.md @@ -121,7 +121,7 @@ For an example of the `IsDisabled` method and custom initialization code when th ## Run Wire (generate code) -Running `make run` calls `make gen-go` on the first run. The `gen-go` in turn calls the Wire binary and generates the code in [`wire_gen.go`](/pkg/server/wire_gen.go). The Wire binary is installed using [`bingo`](https://github.com/bwplotka/bingo) which downloads and installs all the tools needed, including the Wire binary at the specified version. +Running `make run` calls `make gen-go` on the first run. The `gen-go` in turn calls the Wire binary and generates the code in [`wire_gen.go`](/pkg/server/wire_gen.go). The Wire binary is installed using `go tool` which downloads and installs all the tools needed, including the Wire binary at the specified version. ## OSS vs. Enterprise diff --git a/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json b/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json index 943ba719303..1689471193e 100644 --- a/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json +++ b/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json @@ -25,7 +25,6 @@ "fiscalYearStartMonth": 0, "graphTooltip": 0, "links": [], - "liveNow": false, "panels": [ { "datasource": { @@ -42,7 +41,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -156,11 +155,19 @@ "x": -0.8229007633587786, "y": 0.27741935483870966 }, + "sourceOriginal": { + "x": 1223.8335877862596, + "y": 300 + }, "target": { "x": 1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 20", + "targetOriginal": { + "x": 990, + "y": 474 + }, "vertices": [ { "x": -0.18181818181818182, @@ -191,11 +198,19 @@ "x": -0.3068702290076336, "y": -0.47419354838709676 }, + "sourceOriginal": { + "x": 1401.090076335878, + "y": 533 + }, "target": { "x": 1.0112359550561798, "y": -0.4805194805194805 }, "targetName": "Element 20", + "targetOriginal": { + "x": 990, + "y": 492 + }, "vertices": [ { "x": 0.415, @@ -227,11 +242,19 @@ "x": 0.7903930131004366, "y": -0.36935483870967745 }, + "sourceOriginal": { + "x": 1778, + "y": 500.5 + }, "target": { "x": 1.0120481927710843, "y": 0.024691358024691357 }, "targetName": "Element 35", + "targetOriginal": { + "x": 1523, + "y": 177 + }, "vertices": [ { "x": -0.1607843137254902, @@ -518,11 +541,19 @@ "x": 0.8876404494382022, "y": 0.2597402597402597 }, + "sourceOriginal": { + "x": 741, + "y": 356.5 + }, "target": { "x": -0.9775280898876404, "y": -0.012987012987012988 }, - "targetName": "Element 19" + "targetName": "Element 19", + "targetOriginal": { + "x": 813, + "y": 358 + } } ], "constraint": { @@ -614,11 +645,19 @@ "x": 0.8876404494382022, "y": 0.2597402597402597 }, + "sourceOriginal": { + "x": 741, + "y": 239.5 + }, "target": { "x": -0.9887640449438202, "y": -0.06493506493506493 }, - "targetName": "Element 18" + "targetName": "Element 18", + "targetOriginal": { + "x": 812, + "y": 242 + } } ], "constraint": { @@ -673,11 +712,19 @@ "x": -0.2247191011235955, "y": -0.4805194805194805 }, + "sourceOriginal": { + "x": 342, + "y": 395 + }, "target": { "x": 0.011235955056179775, "y": 0.922077922077922 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 343, + "y": 458 + } }, { "color": { @@ -693,11 +740,19 @@ "x": 0.7752808988764045, "y": 0.5194805194805194 }, + "sourceOriginal": { + "x": 431, + "y": 356.5 + }, "target": { "x": -1.0833333333333333, "y": 0.020618556701030927 }, - "targetName": "Element 15" + "targetName": "Element 15", + "targetOriginal": { + "x": 526, + "y": 356 + } } ], "constraint": { @@ -888,11 +943,19 @@ "x": 1, "y": 0.5 }, + "sourceOriginal": { + "x": 431, + "y": 337.25 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 16", + "targetOriginal": { + "x": 562, + "y": 240 + }, "vertices": [ { "x": 0.42748091603053434, @@ -963,11 +1026,19 @@ "x": 0, "y": -1 }, + "sourceOriginal": { + "x": 150, + "y": 223 + }, "target": { "x": -0.5168539325842697, "y": 1 }, "targetName": "Element 6", + "targetOriginal": { + "x": 296, + "y": 318 + }, "vertices": [ { "x": 0, @@ -996,11 +1067,19 @@ "x": 0, "y": 1 }, + "sourceOriginal": { + "x": 150, + "y": 148 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 30", + "targetOriginal": { + "x": 563, + "y": 82 + }, "vertices": [ { "x": 0, @@ -1062,11 +1141,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 216, + "y": 356.5 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, - "targetName": "Element 6" + "targetName": "Element 6", + "targetOriginal": { + "x": 252, + "y": 357 + } } ], "constraint": { @@ -1121,11 +1208,19 @@ "x": 0, "y": -1 }, + "sourceOriginal": { + "x": 342, + "y": 223 + }, "target": { "x": 0.011235955056179775, "y": 1.025974025974026 }, - "targetName": "Element 6" + "targetName": "Element 6", + "targetOriginal": { + "x": 343, + "y": 317 + } } ], "constraint": { @@ -1218,11 +1313,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 38, + "y": 493.5 + }, "target": { "x": -1.0224719101123596, "y": 0.013333333333333334 }, "targetName": "Element 9", + "targetOriginal": { + "x": 59, + "y": 185 + }, "vertices": [ { "x": -0.9523809523809523, @@ -1399,11 +1502,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 563, + "y": 473.5 + }, "target": { "x": 1.0224719101123596, "y": -0.45454545454545453 }, "targetName": "Element 6", + "targetOriginal": { + "x": 433, + "y": 374 + }, "vertices": [ { "x": 0.5692307692307692, @@ -1468,11 +1579,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 989, + "y": 239.5 + }, "target": { "x": -0.96, "y": 0.4583333333333333 }, "targetName": "Element 2", + "targetOriginal": { + "x": 1392, + "y": 487 + }, "vertices": [ { "x": 0.3349875930521092, @@ -1506,11 +1625,19 @@ "x": 1, "y": -0.5 }, + "sourceOriginal": { + "x": 989, + "y": 258.75 + }, "target": { "x": -0.9761904761904762, "y": 0.5263157894736842 }, "targetName": "Element 34", + "targetOriginal": { + "x": 1138, + "y": 290 + }, "vertices": [ { "x": 0.5033557046979866, @@ -1612,11 +1739,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 811, + "y": 473.5 + }, "target": { "x": 1.0112359550561798, "y": 0.012987012987012988 }, - "targetName": "Element 17" + "targetName": "Element 17", + "targetOriginal": { + "x": 742, + "y": 473 + } } ], "constraint": { @@ -1671,11 +1806,19 @@ "x": 1, "y": -0.5 }, + "sourceOriginal": { + "x": 1025.5, + "y": 382.25 + }, "target": { "x": -1.0238095238095237, "y": 0.10526315789473684 }, - "targetName": "Element 33" + "targetName": "Element 33", + "targetOriginal": { + "x": 1136, + "y": 384 + } }, { "color": { @@ -1691,11 +1834,19 @@ "x": 1, "y": 0.5 }, + "sourceOriginal": { + "x": 1025.5, + "y": 333.75 + }, "target": { "x": -1, "y": 0.05263157894736842 }, "targetName": "Element 34", + "targetOriginal": { + "x": 1137, + "y": 299 + }, "vertices": [ { "x": 0.5246636771300448, @@ -1721,11 +1872,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 1025.5, + "y": 358 + }, "target": { "x": -0.98, "y": -0.10416666666666667 }, "targetName": "Element 2", + "targetOriginal": { + "x": 1391, + "y": 514 + }, "vertices": [ { "x": 0.7564979480164159, @@ -1792,11 +1951,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 742, + "y": 81.5 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, - "targetName": "Element 31" + "targetName": "Element 31", + "targetOriginal": { + "x": 811, + "y": 82 + } }, { "color": { @@ -1818,11 +1985,19 @@ "x": 0, "y": 1 }, + "sourceOriginal": { + "x": 653, + "y": 43 + }, "target": { "x": -0.029411764705882353, "y": 1 }, "targetName": "Element 38", + "targetOriginal": { + "x": 1673, + "y": 249 + }, "vertices": [ { "x": 0, @@ -1892,11 +2067,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 990, + "y": 81.5 + }, "target": { "x": -0.9939759036144579, "y": 0.5061728395061729 }, "targetName": "Element 35", + "targetOriginal": { + "x": 1190, + "y": 138 + }, "vertices": [ { "x": 0.48, @@ -1961,11 +2144,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 1221, + "y": 386 + }, "target": { "x": 1.0337078651685394, "y": 0.45454545454545453 }, "targetName": "Element 20", + "targetOriginal": { + "x": 992, + "y": 456 + }, "vertices": [ { "x": -0.05240174672489083, @@ -2067,11 +2258,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 1189, + "y": 179 + }, "target": { "x": 0.19444444444444445, "y": 0.9896907216494846 }, "targetName": "Element 29", + "targetOriginal": { + "x": 1011, + "y": 310 + }, "vertices": [ { "x": 1, @@ -2169,11 +2368,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 1574, + "y": 308 + }, "target": { "x": -0.4977168949771689, "y": 1.017467248908297 }, "targetName": "Element 36", + "targetOriginal": { + "x": 1450, + "y": 384 + }, "vertices": [ { "x": 1, @@ -2208,7 +2415,7 @@ }, "showAdvancedTypes": true }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2236,7 +2443,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "#6ED0E0", @@ -2260,7 +2468,7 @@ "h": 11, "w": 15, "x": 5, - "y": 22 + "y": 36 }, "id": 4, "options": { @@ -2320,11 +2528,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 216, + "y": 192 + }, "target": { "x": -1.02020202020202, "y": 0.041666666666666664 }, - "targetName": "Element 2" + "targetName": "Element 2", + "targetOriginal": { + "x": 326, + "y": 191 + } } ], "constraint": { @@ -2377,11 +2593,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 426, + "y": 168 + }, "target": { "x": -0.9797979797979798, "y": 0 }, - "targetName": "Element 3" + "targetName": "Element 3", + "targetOriginal": { + "x": 549, + "y": 93 + } }, { "color": { @@ -2398,11 +2622,19 @@ "x": 1, "y": -1 }, + "sourceOriginal": { + "x": 426, + "y": 216 + }, "target": { "x": -1, "y": 0.041666666666666664 }, - "targetName": "Element 4" + "targetName": "Element 4", + "targetOriginal": { + "x": 549, + "y": 297 + } } ], "constraint": { @@ -2455,11 +2687,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 647, + "y": 93 + }, "target": { "x": -0.9595959595959596, "y": -0.041666666666666664 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 799, + "y": 94 + } } ], "constraint": { @@ -2512,11 +2752,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 648, + "y": 298 + }, "target": { "x": -0.9797979797979798, "y": 0.041666666666666664 }, - "targetName": "Element 5" + "targetName": "Element 5", + "targetOriginal": { + "x": 798, + "y": 299 + } } ], "constraint": { @@ -2838,7 +3086,7 @@ }, "showAdvancedTypes": false }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "gateway, product-details, product-reviews, reviews-ratings, details-checkout\n100, 56, 44, 22, 28", @@ -2873,7 +3121,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "blue", @@ -2897,7 +3146,7 @@ "h": 15, "w": 15, "x": 5, - "y": 33 + "y": 47 }, "id": 2, "options": { @@ -2949,6 +3198,10 @@ "field": "server_database2", "fixed": "white" }, + "direction": { + "field": "server_database2", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -2959,17 +3212,29 @@ "x": -1, "y": -1 }, + "sourceOriginal": { + "x": 468, + "y": 125 + }, "target": { "x": 0.030303030303030304, "y": 0 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 285, + "y": 170 + } }, { "color": { "field": "server_database", "fixed": "white" }, + "direction": { + "field": "server_database", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -2980,17 +3245,29 @@ "x": -1, "y": -1 }, + "sourceOriginal": { + "x": 468, + "y": 125 + }, "target": { "x": 0.09090909090909091, "y": 0.16666666666666666 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 390, + "y": 264 + } }, { "color": { "field": "server_region", "fixed": "white" }, + "direction": { + "field": "server_region", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3001,11 +3278,19 @@ "x": 1, "y": -1 }, + "sourceOriginal": { + "x": 588.9921875, + "y": 125 + }, "target": { "x": -0.2727272727272727, "y": 0.4166666666666667 }, - "targetName": "Element 13" + "targetName": "Element 13", + "targetOriginal": { + "x": 658, + "y": 259 + } } ], "constraint": { @@ -3050,6 +3335,10 @@ "field": "database_server", "fixed": "white" }, + "direction": { + "field": "database_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3060,11 +3349,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 328, + "y": 367 + }, "target": { "x": -0.15151515151515152, "y": -0.16666666666666666 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 386, + "y": 268 + } } ], "constraint": { @@ -3109,6 +3406,10 @@ "field": "region_server", "fixed": "white" }, + "direction": { + "field": "region_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3119,11 +3420,19 @@ "x": -1, "y": 1 }, + "sourceOriginal": { + "x": 699, + "y": 365 + }, "target": { "x": -0.21212121212121213, "y": 0.08333333333333333 }, - "targetName": "Element 13" + "targetName": "Element 13", + "targetOriginal": { + "x": 659, + "y": 263 + } } ], "constraint": { @@ -3212,6 +3521,10 @@ "field": "database2_server", "fixed": "white" }, + "direction": { + "field": "database2_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3222,11 +3535,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 172, + "y": 191 + }, "target": { "x": -0.3333333333333333, "y": 0 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 279, + "y": 170 + } } ], "constraint": { @@ -3605,7 +3926,7 @@ }, "showAdvancedTypes": false }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "database_server, server_database, server_region, region_server, database2_server, server_database2\n10, 53, 35, 12, 22, 81", @@ -3620,9 +3941,9 @@ "type": "canvas" } ], + "preload": false, "refresh": "", - "revision": 1, - "schemaVersion": 39, + "schemaVersion": 41, "tags": ["gdev", "panel-tests", "canvas"], "templating": { "list": [] @@ -3631,11 +3952,9 @@ "from": "now-6h", "to": "now" }, - "timeRangeUpdatedDuringEditOrView": false, "timepicker": {}, "timezone": "", "title": "Panel Tests - Canvas Connection Examples", "uid": "Pu8lwQAVz", - "version": 6, - "weekStart": "" + "version": 15 } diff --git a/docs/sources/administration/plugin-management/_index.md b/docs/sources/administration/plugin-management/_index.md index 77e9e57f26f..fce84ec643e 100644 --- a/docs/sources/administration/plugin-management/_index.md +++ b/docs/sources/administration/plugin-management/_index.md @@ -30,6 +30,16 @@ Read on for an overview on how to get started with plugins: - Configure backend communication between installed plugins. - Improve security by isolating plugins with the Plugin Frontend Sandbox. +## Types of plugins + +Grafana supports three types of plugins: + +- [Panels](/grafana/plugins/panel-plugins) - These plugins make it easy to create and add any kind of visualization, to show your data, or improve your favorite dashboards. +- [Data sources](/grafana/plugins/data-source-plugins) - These plugins allow you to pull data from various data sources such as databases, APIs, log files, and so on, and display it in the form of graphs, charts, and dashboards in Grafana. +- [Apps](/grafana/plugins/app-plugins) - These plugins enable the bundling of data sources, panels, dashboards, and Grafana pages into a cohesive experience. + +Read more in [Types of plugins](plugin-types). + ## Plugin catalog The Grafana plugin catalog allows you to browse and manage plugins from within Grafana. Only Grafana server administrators and Organization administrators can access and use the plugin catalog. For more information about Grafana roles and permissions, refer to [Roles and permissions](../roles-and-permissions/). @@ -40,41 +50,12 @@ The following access rules apply depending on the user role: - If you are a **Server Admin**, you can't configure app plugins, but you can install, uninstall, or update them. - If you are both **Org Admin** and **Server Admin**, you can configure app plugins and also install, uninstall, or update them. -{{< admonition type="note" >}} -The Grafana plugin catalog is designed to work with a single Grafana server instance only. Support for Grafana clusters is planned for future Grafana releases. -{{< /admonition >}} - -
- -
- -_Video shows the Plugin catalog in a previous version of Grafana._ - -{{< admonition type="note" >}} -If required, the Grafana plugin catalog can be disabled using the `plugin_admin_enabled` flag in the [configuration](../../setup-grafana/configure-grafana/#plugin_admin_enabled) file. -{{< /admonition >}} - - - -### Types of plugins - -Grafana supports three types of plugins: - -- [Panels](/grafana/plugins/panel-plugins) - These plugins make it easy to create and add any kind of panel, to show your data, or improve your favorite dashboards. -- [Data sources](/grafana/plugins/data-source-plugins) - These plugins allow you to pull data from various data sources such as databases, APIs, log files, and so on, and display it in the form of graphs, charts, and dashboards in Grafana. -- [Apps](/grafana/plugins/app-plugins) - These plugins enable the bundling of data sources, panels, dashboards, and Grafana pages into a cohesive experience. - -Read more in [Types of plugins](plugin-types). - ### Browse plugins To browse for available plugins: 1. While logged into Grafana as an administrator, click **Administration > Plugins and data > Plugins** in the side menu to view installed and available plugins. -1. Use the search to filter based on name, keywords, organization and other metadata. +1. Use the search box to filter based on name, keywords, organization and other metadata. 1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type. ## Manage your plugins @@ -126,7 +107,7 @@ Plugin signature verification, also known as _signing_, is a security measure to You can relocate app plugin pages to customize the navigation menu structure, as explained in [Customize navigation placement of plugin pages](customize-nav-bar). -### Integrate plugins +### Allow plugin backend communication You can configure your Grafana instance to let the frontends of installed plugins directly communicate locally with the backends of other installed plugins. See how in [Configure backend communication between installed plugins](plugin-integrate). @@ -134,6 +115,13 @@ You can configure your Grafana instance to let the frontends of installed plugin You can use the [Plugin Frontend Sandbox](plugin-frontend-sandbox) to securely isolate plugin frontend code from the main Grafana application. +When enabled, plugins run in a separate JavaScript context, which provides several security benefits: + +- Prevents plugins from modifying parts of the Grafana interface outside their designated areas +- Stops plugins from interfering with other plugins functionality +- Protects core Grafana features from being altered by plugins +- Prevents plugins from modifying global browser objects and behaviors + ### Learn more - [Browse available plugins](/grafana/plugins) diff --git a/docs/sources/administration/plugin-management/plugin-types.md b/docs/sources/administration/plugin-management/plugin-types.md index fe8cf4de0ff..bd1f53e18a3 100644 --- a/docs/sources/administration/plugin-management/plugin-types.md +++ b/docs/sources/administration/plugin-management/plugin-types.md @@ -45,6 +45,12 @@ Data source plugins communicate with external sources of data and return the dat Use data source plugins when you want to query data from external or third-party systems. +### Managing access for data source plugins + +Administrators can grant access to data source plugins with [LBAC](/docs/grafana//administration/data-source-management/teamlbac/). + +Label Based Access Control (LBAC) customizes access rights based on team memberships, ensuring that users only query data relevant to their assigned permissions. + ## App plugins Applications, or app plugins, bundle data sources and panels to provide a cohesive experience, such as the [Zabbix](/grafana/plugins/alexanderzobnin-zabbix-app) app. @@ -57,7 +63,9 @@ Use app plugins when you want an out-of-the-box monitoring experience. Customize access to app plugins with [RBAC](/docs/grafana//administration/roles-and-permissions/access-control/rbac-for-app-plugins/). -By default, the Viewer, Editor and Admin roles have access to all app plugins that their Organization role allows them to access. Access is granted by the `fixed:plugins.app:reader` role. +By default, the Viewer, Editor and Admin roles have access to all app plugins that their Organization role allows them to access. + +Access is granted by the `fixed:plugins.app:reader` role. {{< admonition type="note" >}} To prevent users from seeing an app plugin, refer to [these permissions scenarios](/docs/grafana//administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/#prevent-viewers-from-accessing-an-app-plugin). diff --git a/docs/sources/dashboards/manage-dashboards/index.md b/docs/sources/dashboards/manage-dashboards/index.md index e09f2837eaa..7e8ff1bd941 100644 --- a/docs/sources/dashboards/manage-dashboards/index.md +++ b/docs/sources/dashboards/manage-dashboards/index.md @@ -81,12 +81,16 @@ Folders help you organize and group dashboards, which is useful when you have ma 1. Enter a unique name and click **Create**. + Folder names can't include underscores (\_) or percentage signs (%), as it interferes with the search functionality. + + Also, alerts can't be placed in folders with slashes (\ /) in the name. If you want to place alerts in the folder, don't use slashes in the folder name. + When you nest folders, you can do so up to four levels deep. When you save a dashboard, you can optionally select a folder to save the dashboard in. {{< admonition type="note" >}} -Alerts can't be placed in folders with slashes (\ /) in the name. If you wish to place alerts in the folder, don't use slashes in the folder name. + {{< /admonition >}} **To edit the name of a folder:** diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index c2ec4225c03..3645aed2ef4 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -688,7 +688,7 @@ }, "languages": { "type": "array", - "description": "The list of languages supported by the plugin. Each entry should be a locale identifier in the format `language-COUNTRY` (e.g., `en-US`, `fr-FR`, `es-ES`).", + "description": "The list of languages supported by the plugin. Each entry should be a locale identifier in the format `language-COUNTRY` (for example `en-US`, `fr-FR`, `es-ES`).", "items": { "type": "string" } diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 63daf9b63e8..0c0821cc923 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -481,12 +481,19 @@ You can style the selected connection using the following options: - **Color** - Set the connection color. - **Size** - Control the size of the connection by entering a number in the **Value** field. - **Radius** - Add curve to the connection by entering a value to represent the degree. -- **Arrow Direction** - Control the appearance of the arrow head. Choose from: +- **Direction** - Control the appearance of the arrow head. Choose your source from **Fixed** or **Field**. The default value is **Forward** regardless of the source type. + + If the direction source is **Fixed**, choose from: - **Forward** - The arrow head points in the direction in which the connection was drawn. - **Reverse** - The arrow head points in the opposite direction of which the connection was drawn. - **Both** - Adds arrow heads to both ends of the connection. - **None** - Removes the arrow head. + If the direction source is **Field**, select a field that contains numeric values: + - **Positive values** - Display forward arrows. + - **Negative values** - Display reverse arrows. + - **Zero** - Display no arrow heads. + - **Line style** - Choose from the following line styles: **Solid**, **Dashed**, and **Dotted**. ### Standard options diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md index fb9511a2ac5..030b002760b 100644 --- a/docs/sources/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md @@ -281,7 +281,7 @@ The markers layer allows you to display data points as different marker shapes s | Symbol Horizontal Align | Configures the horizontal alignment of the symbol relative to the data point. Note that the symbol's rotation angle is applied first around the data point, then the horizontal alignment is applied relative to the rotation of the symbol. | | Color | Configures the color of the markers. The default `Fixed color` sets all markers to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. | | Fill opacity | Configures the transparency of each marker. | -| Rotation angle | Configures the rotation angle of each marker. The default is `Fixed value`, which makes all markers rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the markers based on data corresponding to a selected field. | +| Rotation angle | Configures the rotation angle of each marker in degrees. The default is `Fixed value`, which makes all markers rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the markers based on data corresponding to a selected field. | | Text label | Configures a text label for each marker. | | Show legend | Allows you to toggle the legend for the layer. | | Display tooltip | Allows you to toggle tooltips for the layer. | @@ -448,7 +448,7 @@ You can convert node graph data to a network layer: | Symbol | Allows you to choose the symbol, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple symbols or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). | | Color | Configures the color of the nodes. The default `Fixed color` sets all nodes to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. | | Fill opacity | Configures the transparency of each node. | -| Rotation angle | Configures the rotation angle of each node. The default is `Fixed value`, which makes all nodes rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the nodes based on data corresponding to a selected field. | +| Rotation angle | Configures the rotation angle of each node in degrees. The default is `Fixed value`, which makes all nodes rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the nodes based on data corresponding to a selected field. | | Text label | Configures a text label for each node. | diff --git a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md index 9b6fad3ed4a..c7570c71eaa 100644 --- a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md +++ b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md @@ -198,6 +198,14 @@ List of enabled loggers. Keep dashboard content in the logs (request or response fields). This can significantly increase the size of your logs. +### log_datasource_query_request_body + +Whether to record data source queries' request body. This can significantly increase the size of your logs. Enabled by default. + +### log_datasource_query_response_body + +Whether to record data source queries' response body. This can significantly increase the size of your logs. Enabled by default. + ### verbose Log all requests and keep requests and responses body. This can significantly increase the size of your logs. diff --git a/docs/sources/setup-grafana/configure-security/audit-grafana.md b/docs/sources/setup-grafana/configure-security/audit-grafana.md index 624c81432c4..44f1ea7d5e2 100644 --- a/docs/sources/setup-grafana/configure-security/audit-grafana.md +++ b/docs/sources/setup-grafana/configure-security/audit-grafana.md @@ -371,6 +371,10 @@ enabled = false loggers = file # Keep dashboard content in the logs (request or response fields); this can significantly increase the size of your logs. log_dashboard_content = false +# Whether to record data source queries' request body. This can significantly increase the size of your logs. Enabled by default. +log_datasource_query_request_body = true +# Whether to record data source queries' response body. This can significantly increase the size of your logs. Enabled by default. +log_datasource_query_response_body = true # Keep requests and responses body; this can significantly increase the size of your logs. verbose = false # Write an audit log for every status code. 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 b7136c047b3..4ecb9c2cdc8 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 @@ -143,7 +143,7 @@ If set to `true`, the Grafana server redirects requests that have a Host-header Example: ```toml -[sever] +[server] # Redirect to correct domain if host header does not match domain # Prevents DNS rebinding attacks enforce_domain = true diff --git a/docs/sources/upgrade-guide/when-to-upgrade/index.md b/docs/sources/upgrade-guide/when-to-upgrade/index.md index 6467fb25ded..1ffaf22fd31 100644 --- a/docs/sources/upgrade-guide/when-to-upgrade/index.md +++ b/docs/sources/upgrade-guide/when-to-upgrade/index.md @@ -68,7 +68,7 @@ Grafana currently follows a monthly release schedule. Below are the planned rele - The schedule above outlines how we plan release dates. However, unforeseen events and circumstances may cause dates to change. - High severity security and feature degradation incidents will result in ad-hoc releases that are not scheduled ahead of time. -- Patching releases are for the current (last released) minor version of Grafana. Additional older versions of Grafana may be included if there is a critical bug or security vulnerability that needs to be patched. +- Patching releases are for all supported minor versions of Grafana. Each supported minor version receives patch releases with bug fixes and security patches until its end of life. - Release freezes: Each year Grafana implements two release freezes to accommodate for the holiday season. During these times, no scheduled releases will be executed. However, this does not apply to changes that may be required during the course of an operational or security incident. ## Grafana security releases: improved version naming convention @@ -96,47 +96,63 @@ For self-managed Grafana (both Enterprise and OSS), the support for versions fol - Each minor release is supported for 9 months after its release date - The last minor release of a major version receives extended support for 15 months after its release date - Support levels change as new versions are released: - - **Full Support**: The most recently released major/minor version receive full support including new features, bug fixes, and security patches - - **Security & Critical Bugs Only**: Versions that are outside of the most recently released major/minor version, but still within their support period, receive only security patches and critical bug fixes + - **Full Support**: The current major version receives new features through new minor releases + - **Patch Support**: Individual minor versions receive patch releases (bug fixes and security patches) until end of life - **Not Supported**: Versions beyond their support period receive no updates Here is an overview of version support through 2026: -| **Version** | **Release date** | **Support end date** | **Support level** | -| ------------------------- | ------------------ | -------------------- | ----------------------------- | -| 10.2.x | October 24, 2023 | July 24, 2024 | Not Supported | -| 10.3.x | January 23, 2024 | October 23, 2024 | Not Supported | -| 10.4.x (Last minor of 10) | March 5, 2024 | June 5, 2025 | Not Supported | -| 11.0.x | May 14, 2024 | February 14, 2025 | Not Supported | -| 11.1.x | June 25, 2024 | April 23, 2025 | Not Supported | -| 11.2.x | August 27, 2024 | May 27, 2025 | Not Supported | -| 11.3.x | October 22, 2024 | July 22, 2025 | Not Supported | -| 11.4.x | December 5, 2024 | September 5, 2025 | Security & Critical Bugs Only | -| 11.5.x | January 28, 2025 | October 28, 2025 | Security & Critical Bugs Only | -| 11.6.x (Last minor of 11) | March 25, 2025 | June 25, 2026 | Security & Critical Bugs Only | -| 12.0.x | May 5, 2025 | February 5, 2026 | Security & Critical Bugs Only | -| 12.1.x | July 22, 2025 | April 22, 2026 | Full Support until next minor | -| 12.2.x | September 23, 2025 | June 23, 2026 | Yet to be released | -| 12.3.x | November 18, 2025 | August 18, 2026 | Yet to be released | +| **Version** | **Release date** | **Support end date** | **Support level** | +| ------------------------- | ------------------ | -------------------- | ------------------ | +| 10.2.x | October 24, 2023 | July 24, 2024 | Not Supported | +| 10.3.x | January 23, 2024 | October 23, 2024 | Not Supported | +| 10.4.x (Last minor of 10) | March 5, 2024 | June 5, 2025 | Not Supported | +| 11.0.x | May 14, 2024 | February 14, 2025 | Not Supported | +| 11.1.x | June 25, 2024 | April 23, 2025 | Not Supported | +| 11.2.x | August 27, 2024 | May 27, 2025 | Not Supported | +| 11.3.x | October 22, 2024 | July 22, 2025 | Not Supported | +| 11.4.x | December 5, 2024 | September 5, 2025 | Patch Support | +| 11.5.x | January 28, 2025 | October 28, 2025 | Patch Support | +| 11.6.x (Last minor of 11) | March 25, 2025 | June 25, 2026 | Patch Support | +| 12.0.x | May 5, 2025 | February 5, 2026 | Patch Support | +| 12.1.x | July 22, 2025 | April 22, 2026 | Patch Support | +| 12.2.x | September 23, 2025 | June 23, 2026 | Yet to be released | +| 12.3.x | November 18, 2025 | August 18, 2026 | Yet to be released | ## How are these versions supported? -The level of support changes as new versions of Grafana are released. Here are the key details: +Self-managed Grafana follows semantic-like versioning (MAJOR.MINOR.PATCH). Here's how different types of releases work: -- **Full Support**: - - All new features - - All bug fixes - - Security patches - - Regular updates +- **Major releases** (e.g., 12.0.0, 13.0.0): + - Awesome new features as well as significant architectural improvements and modernizations + - May include breaking changes that require migration steps + - Released once per year -- **Security & Critical Bugs Only**: - - Security vulnerability patches - - Critical bug fixes that cause feature degradation - - No new features +- **Minor releases** (e.g., 12.3.0, 12.4.0): + - Contain new features and enhancements + - Include bug fixes and security patches + - Released every other month + +- **Patch releases** (e.g., 12.3.1, 12.3.2): + - **Only** contain bug fixes and security patches + - **No new features** - these wait until the next minor release + - Released monthly + +**Support levels by version:** + +- **Full Support** (current major version): + - Gets new minor releases with new features approximately every other month + - All minor versions within the major receive patch releases until end of life + - Example: Major 12.x gets new features via 12.1.0, 12.2.0, 12.3.0, etc. + +- **Patch Support** (individual minor versions): + - Each minor version receives patch releases (bug fixes and security patches) until end of life + - No new features - these only come with new minor releases + - Example: 12.3.x gets 12.3.1, 12.3.2, etc. with fixes only - **Not Supported**: Versions beyond their support period receive no updates and should be upgraded. -Keeping all this in mind, users that want to receive the most recent features and all bug fixes should be on the current (most recently released) version of Grafana. +**Example**: When 12.3.0 is released, it includes new features. Subsequent releases like 12.3.1 and 12.3.2 only include bug fixes and security updates. All new features developed after 12.3.0 wait until 12.4.0 is released. ### What is a critical feature degradation? diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts index a7ef95b1f4d..d483b7b8005 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts @@ -34,7 +34,7 @@ test.describe( const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/); - await page.getByRole('switch', { name: 'Transparent background' }).click({ force: true }); + await page.locator('#transparent-background').click({ force: true }); const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/); diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 739646813f8..fc24bfd6832 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -95,8 +95,8 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Cell options Cell value inspect')) .first() - .getByRole('switch', { name: 'Cell value inspect' }) - .click({ force: true }); + .locator('label[for="custom.inspect"]') + .click(); await loremIpsumCell.hover(); await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); @@ -133,6 +133,15 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] await displayNameInput.fill('State (renamed)'); await displayNameInput.press('Enter'); expect(page.getByRole('row').nth(0)).toContainText('State (renamed)'); + + // toggle the "State" column visibility again to hide it again. this confirms that we avoid bugs related to + // array lengths between the fields array and the column widths array. + await hideStateColumnSwitch.click(); + expect(page.getByRole('row').nth(0)).not.toContainText('State'); + + // since the previous assertion is just for the absence of text, let's also confirm that the table is + // actually still on the page and that an error has not been throw. + await waitForTableLoad(page); }); // we test niche cases for sorting, filtering, pagination, etc. in a unit tests already. diff --git a/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts index fcadcb55a47..b344dae5abe 100644 --- a/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts @@ -17,35 +17,35 @@ describe('Geomap layer controls options', () => { e2e.components.PanelEditor.showZoomField() .should('be.visible') .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }); + cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); }); // Show attribution e2e.components.PanelEditor.showAttributionField() .should('be.visible') .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }); + cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); }); // Show scale e2e.components.PanelEditor.showScaleField() .should('be.visible') .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }); + cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); }); // Show measure tool e2e.components.PanelEditor.showMeasureField() .should('be.visible') .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }); + cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); }); // Show debug e2e.components.PanelEditor.showDebugField() .should('be.visible') .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }); + cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); }); e2e.components.Panels.Panel.content({ timeout: TIMEOUT }) diff --git a/eslint.config.js b/eslint.config.js index 664de667de6..f0958ee1346 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,8 +8,6 @@ const jsxA11yPlugin = require('eslint-plugin-jsx-a11y'); const lodashPlugin = require('eslint-plugin-lodash'); const barrelPlugin = require('eslint-plugin-no-barrel-files'); const reactPlugin = require('eslint-plugin-react'); -// FIXME: Remove once eslint-config-grafana is updated to include correct plugin -const hooksPlugin = require('eslint-plugin-react-hooks'); const testingLibraryPlugin = require('eslint-plugin-testing-library'); const unicornPlugin = require('eslint-plugin-unicorn'); @@ -60,17 +58,7 @@ module.exports = [ 'public/build-swagger', // swagger build output ], }, - // FIXME: Remove once eslint-config-grafana is updated to include correct plugin - { - name: 'react-hooks-plugin', - plugins: { - 'react-hooks': hooksPlugin, - }, - }, - // Conditionally run the betterer rules if enabled in dev's config - ...(enableBettererRules ? bettererConfig : []), - // FIXME: Remove filtering once eslint-config-grafana is updated to include correct plugin - ...grafanaConfig.filter(Boolean), + ...grafanaConfig, { name: 'react/jsx-runtime-rules', rules: reactPlugin.configs.flat['jsx-runtime'].rules, @@ -176,6 +164,8 @@ module.exports = [ ], // FIXME: Fix these in follow up PR 'react/no-unescaped-entities': 'off', + // Turn off react-hooks/rules-of-hooks whilst present in betterer + 'react-hooks/rules-of-hooks': 'off', }, }, { @@ -455,4 +445,7 @@ module.exports = [ ], }, }, + // Conditionally run the betterer rules if enabled in dev's config + // Should be last in the config so it can override any temporary disables in here + ...(enableBettererRules ? bettererConfig : []), ]; diff --git a/go.mod b/go.mod index 22f0a410166..66b4567293d 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 28226af3d36..58964ba56d1 100644 --- a/go.sum +++ b/go.sum @@ -1580,8 +1580,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1 h1:nn19bwMFBGWOvUTxZsdewOfRiXERhKHp0jEira8uQA8= -github.com/grafana/alerting v0.0.0-20250812203446-a38b2187d4e1/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= +github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0 h1:CC+ShIgoLTBsAulRfJH9xF++Us6Y3va3VFbeDeJym2M= +github.com/grafana/alerting v0.0.0-20250821181654-10915888e4f0/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= diff --git a/go.work.sum b/go.work.sum index c679fb56a1b..8fbfb8dfd88 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1095,6 +1095,8 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGU github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= 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.67.0 h1:t0BiJZVW9D3Z16y3uHqKzV9bKFTusooTH1Kgr77xF2Q= +github.com/jaegertracing/jaeger v1.67.0/go.mod h1:tE/FEQfybCSdUbBgel51YaCSkc58O+Njih8oTl6j8vw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= @@ -1268,6 +1270,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.1 github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.121.0/go.mod h1:/+tb7Hkt0i92WFkkfHurT7rXrZ98nxat5AeGmbBYFm4= github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121.0 h1:Q27uarV8CEGLKfG4OJQGFkOKZ49/SDEqTr6BbMOoKmA= github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121.0/go.mod h1:f9MPSdMvooe2s9Zg0WVBJOvg72d9fFikxFt7yniFros= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1/go.mod h1:AFMryJmht7dZxcAwc2sx/r4gxbriElWw49ugxKp2mcA= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.121.0 h1:I+F6xdXQsiXXdce7yjHN+y4LX5MrZI1kNmhBunJffdA= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.121.0/go.mod h1:cRh3l2emFBwW96dHnlPLr1psbEYjYJmn5qFujOkbfRo= github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.124.1 h1:2uQmRiQ7EV7s1slz7fEvAVhJIFTyExnj/4unfw0Era8= @@ -1280,6 +1284,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcompone github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.124.1/go.mod h1:jRQpw8VJDmUmyH8ONQ5FmhYEXkaxzMFX6Tei8N4GDZs= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0 h1:dnRf9xyjTBGHui949SS0A87fNa49Al32dog9XmeGY7I= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0/go.mod h1:ZAFQ1Te56fodNun+lmC/jFjVxhecumV882ct8sbdeAM= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 h1:E1e96GTHmiAfIfeYfA5ZVnOxud3+vbisGp0gE1tfd4s= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1/go.mod h1:MOhFATtYSLad9nKunjh6uGf8nQUcWje2LPlhD2uu3do= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1 h1:QX88VjAiUaG7SQe4A14b3JPgvzZK6OXpWoBd32Zlp1o= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1/go.mod h1:jRsmTRbn1HkrOpoeXS0KGfXobrh7E3W/p9fLwLdpcB8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.121.0 h1:7xzxbwIBpb8LlMNY+RIdEnWTJH+pDu0LiL+tcIziEkA= @@ -1296,6 +1302,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0 h1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0/go.mod h1:PQjfR4WB9Q1MR9X/6IqxWGpKfavp9OF1fUcAqnPGGpQ= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1 h1:kPPjaOlncaJjoaJRUzW3TbqHM4v/+TtxC2Bzpa7z9X4= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1/go.mod h1:+6zqsZ1YGNsl7RUxfjzg7MKEWd5J6QAlq7zM3Nqy8UI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 h1:9c6L4xlAMqhOg5y54Bc2B5t0i49yz7v2I6I8RY4Z0/o= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1/go.mod h1:6f0N58o0cOHC0ApSM/qrooVmQza1eQ7L53PDE91uO1Q= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1 h1:IYxc8uPVCtKlgxjbYFGZ5/wXLdwAH1WQPKRlzaXtr8E= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1/go.mod h1:jFRiSEn2ss3e4CWG6GTDOJMYRI0C/N2XbMMIBzZuqZY= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.121.0 h1:QzV771eoAx2W9HCFq/YJVMNgEuG4lvalPTQIutheFhc= @@ -1535,6 +1543,7 @@ go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= go.opentelemetry.io/collector v0.124.0/go.mod h1:QzERYfmHUedawjr8Ph/CBEEkVqWS8IlxRLAZt+KHlCg= go.opentelemetry.io/collector/client v1.30.0 h1:QbvOrvwUGcnVjnIBn2zyLLubisOjgh7kMgkzDAiYpHg= go.opentelemetry.io/collector/client v1.30.0/go.mod h1:msXhZlNdAra2fZiyeT0o/xj43Kl1yvF9zYW0r+FhGUI= @@ -1636,6 +1645,8 @@ go.opentelemetry.io/collector/internal/telemetry v0.124.0 h1:kzd1/ZYhLj4bt2pDB52 go.opentelemetry.io/collector/internal/telemetry v0.124.0/go.mod h1:ZjXjqV0dJ+6D4XGhTOxg/WHjnhdmXsmwmUSgALea66Y= go.opentelemetry.io/collector/otelcol v0.124.0 h1:q/+ebTZgEZX+yFbvO7FeqpEtvtRPJ+YzZzHsVzqA71s= go.opentelemetry.io/collector/otelcol v0.124.0/go.mod h1:mFGJZn5YuffdMVO/lPBavbW+R64Dgd3jOMgw2WAmJEM= +go.opentelemetry.io/collector/pdata/pprofile v0.124.0 h1:ZjL9wKqzP4BHj0/F1jfGxs1Va8B7xmYayipZeNVoWJE= +go.opentelemetry.io/collector/pdata/pprofile v0.124.0/go.mod h1:1EN3Gw5LSI4fSVma/Yfv/6nqeuYgRTm1/kmG5nE5Oyo= go.opentelemetry.io/collector/pdata/testdata v0.124.0 h1:vY+pWG7CQfzzGSB5+zGYHQOltRQr59Ek9QiPe+rI+NY= go.opentelemetry.io/collector/pdata/testdata v0.124.0/go.mod h1:lNH48lGhGv4CYk27fJecpsR1zYHmZjKgNrAprwjym0o= go.opentelemetry.io/collector/pipeline v0.124.0 h1:hKvhDyH2GPnNO8LGL34ugf36sY7EOXPjBvlrvBhsOdw= @@ -1668,6 +1679,8 @@ go.opentelemetry.io/collector/receiver/receivertest v0.124.0 h1:mx0290aXAo+wfjm4 go.opentelemetry.io/collector/receiver/receivertest v0.124.0/go.mod h1:3RpopRmIzx5T4zTStHJC0HHfd8YFWm8e9bia1HiuDtY= go.opentelemetry.io/collector/receiver/xreceiver v0.124.0 h1:YigTUKk8p/aIfqaT0ST7teT9KbLThWD5n2km83byftw= go.opentelemetry.io/collector/receiver/xreceiver v0.124.0/go.mod h1:NkTpmpAEDT17Dko4gpHUnRztrSkdSd6B0+Y4gfuCWIA= +go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/9dNZ0rv454goA= +go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/collector/service v0.124.0 h1:lUpizko/Y2P+XXbZ9wiKM8acLSt6ZIvC3/6/j6rcq4w= go.opentelemetry.io/collector/service v0.124.0/go.mod h1:w2eL3KKOMW4CvqCWyZ3P/Qh1ZBEPGG/uRz/0LpHbpv0= go.opentelemetry.io/collector/service/hostcapabilities v0.124.0 h1:ArxbARF7+bnzK8xLnN2G41KInbcN1aGhSBR76VeUQi8= diff --git a/kindsv2/Makefile b/kindsv2/Makefile index 1e72c98cd5f..92c09866e6b 100644 --- a/kindsv2/Makefile +++ b/kindsv2/Makefile @@ -1,5 +1,3 @@ -include ../.bingo/Variables.mk - .PHONY: all all: dashboards diff --git a/package.json b/package.json index 42ecab60013..388b925397a 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "@crowdin/crowdin-api-client": "^1.42.0", "@cypress/webpack-preprocessor": "6.0.4", "@emotion/eslint-plugin": "11.12.0", - "@grafana/eslint-config": "8.1.0", + "@grafana/eslint-config": "8.2.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/plugin-e2e": "2.1.7", "@grafana/test-utils": "workspace:*", @@ -195,7 +195,7 @@ "eslint-plugin-lodash": "8.0.0", "eslint-plugin-no-barrel-files": "^1.1.1", "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "5.1.0", + "eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-testing-library": "^7.0.0", "eslint-plugin-unicorn": "^56.0.0", "eslint-scope": "^8.1.0", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index cc2d2b8b63b..4cd790e7148 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -197,10 +197,6 @@ export interface FeatureToggles { */ provisioning?: boolean; /** - * Experimental feature to use the secrets service for provisioning instead of the legacy secrets - */ - provisioningSecretsService?: boolean; - /** * Start an additional https handler and write kubectl options */ grafanaAPIServerEnsureKubectlAccess?: boolean; diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 71b93ab7e39..94f1d97518c 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -137,6 +137,8 @@ export interface LogRowContextOptions { direction?: LogRowContextQueryDirection; limit?: number; scopedVars?: ScopedVars; + // Optional. Size of the time window to get logs before of after the referenced entry. + timeWindowMs?: number; } export enum LogRowContextQueryDirection { @@ -181,6 +183,9 @@ export interface DataSourceWithLogsContextSupport { diff --git a/packages/grafana-runtime/src/components/FolderPicker.tsx b/packages/grafana-runtime/src/components/FolderPicker.tsx index 3a22b761e96..2e129480908 100644 --- a/packages/grafana-runtime/src/components/FolderPicker.tsx +++ b/packages/grafana-runtime/src/components/FolderPicker.tsx @@ -13,6 +13,9 @@ interface FolderPickerProps { /* Folder UIDs to exclude from the picker, to prevent invalid operations */ excludeUIDs?: string[]; + /* Start tree from this folder instead of root */ + rootFolderUID?: string; + /* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */ permission?: 'view' | 'edit'; diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 5be227ba4b9..1931a6ad967 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -111,6 +111,26 @@ export enum ResourceDimensionMode { Mapping = 'mapping', } +/** + * Links to a resource (image/svg path) + */ +export interface ResourceDimensionConfig extends BaseDimensionConfig { + fixed?: string; + mode: ResourceDimensionMode; +} + +export enum ConnectionDirection { + Both = 'both', + Forward = 'forward', + None = 'none', + Reverse = 'reverse', +} + +export enum DirectionDimensionMode { + Field = 'field', + Fixed = 'fixed', +} + export interface MapLayerOptions { /** * Custom options depending on the type @@ -899,12 +919,9 @@ export interface DataSourceRef { uid?: string; } -/** - * Links to a resource (image/svg path) - */ -export interface ResourceDimensionConfig extends BaseDimensionConfig { - fixed?: string; - mode: ResourceDimensionMode; +export interface DirectionDimensionConfig extends BaseDimensionConfig { + fixed?: ConnectionDirection; + mode: DirectionDimensionMode; } export interface FrameGeometrySource { diff --git a/packages/grafana-schema/src/common/dimensions.cue b/packages/grafana-schema/src/common/dimensions.cue index 08c0b8ac452..0ffd7d83b48 100644 --- a/packages/grafana-schema/src/common/dimensions.cue +++ b/packages/grafana-schema/src/common/dimensions.cue @@ -35,7 +35,7 @@ TextDimensionMode: "fixed" | "field" | "template" @cuetsy(kind="enum") TextDimensionConfig: { BaseDimensionConfig mode: TextDimensionMode - fixed?: string + fixed?: string }@cuetsy(kind="interface") ResourceDimensionMode: "fixed" | "field" | "mapping" @cuetsy(kind="enum") @@ -44,6 +44,15 @@ ResourceDimensionMode: "fixed" | "field" | "mapping" @cuetsy(kind="enum") ResourceDimensionConfig: { BaseDimensionConfig mode: ResourceDimensionMode - fixed?: string + fixed?: string }@cuetsy(kind="interface") +ConnectionDirection: "forward" | "reverse" | "both" | "none" @cuetsy(kind="enum", memberNames="Forward|Reverse|Both|None") + +DirectionDimensionMode: "fixed" | "field" @cuetsy(kind="enum") + +DirectionDimensionConfig: { + BaseDimensionConfig + mode: DirectionDimensionMode + fixed?: ConnectionDirection +}@cuetsy(kind="interface") 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 997afed4975..60cb7a2adcd 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 @@ -80,6 +80,7 @@ export enum ConnectionPath { export interface CanvasConnection { color?: ui.ColorDimensionConfig; + direction?: ui.DirectionDimensionConfig; path: ConnectionPath; size?: ui.ScaleDimensionConfig; source: ConnectionCoordinates; diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index 3e626ba021b..4b4bf959496 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -1,9 +1,22 @@ import { HttpHandler } from 'msw'; import folderHandlers from './api/folders/handlers'; +import searchHandlers from './api/search/handlers'; import teamsHandlers from './api/teams/handlers'; -import appPlatformFolderHandlers from './apis/dashboard.grafana.app/v0alpha1/handlers'; +import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v0alpha1/handlers'; +import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers'; +import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers'; -const allHandlers: HttpHandler[] = [...teamsHandlers, ...folderHandlers, ...appPlatformFolderHandlers]; +const allHandlers: HttpHandler[] = [ + // Legacy handlers + ...teamsHandlers, + ...folderHandlers, + ...searchHandlers, + + // App platform handlers + ...appPlatformDashboardv0alpha1Handlers, + ...appPlatformFolderv1beta1Handlers, + ...appPlatformIamv0alpha1Handlers, +]; export default allHandlers; diff --git a/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts index 103b816ce3a..6da13af3efe 100644 --- a/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts @@ -1,3 +1,4 @@ +import { Chance } from 'chance'; import { HttpResponse, http } from 'msw'; import { treeViewersCanEdit, wellFormedTree } from '../../../fixtures/folders'; @@ -6,6 +7,26 @@ const [mockTree] = wellFormedTree(); const [mockTreeThatViewersCanEdit] = treeViewersCanEdit(); const collator = new Intl.Collator(); +// TODO: Generalise access control response and additional properties +const mockAccessControl = { + 'dashboards.permissions:write': true, + 'dashboards:create': true, +}; +const additionalProperties = { + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: '2025-07-14T12:07:36+02:00', + createdBy: 'Anonymous', + hasAcl: false, + orgId: 1, + updated: '2025-07-15T18:01:36+02:00', + updatedBy: 'Anonymous', + url: '/grafana/dashboards/f/1ca93012-1ffc-5d64-ae2e-54835c234c67/rik-cujahda-pi', + version: 1, +}; + const listFoldersHandler = () => http.get('/api/folders', ({ request }) => { const url = new URL(request.url); @@ -21,7 +42,9 @@ const listFoldersHandler = () => const folders = tree .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) .map((folder) => { + const random = Chance(folder.item.uid); return { + id: random.integer({ min: 1, max: 1000 }), uid: folder.item.uid, title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", }; @@ -33,17 +56,25 @@ const listFoldersHandler = () => }); const getFolderHandler = () => - http.get('/api/folders/:uid', ({ params }) => { + http.get('/api/folders/:uid', ({ params, request }) => { const { uid } = params; + const url = new URL(request.url); + const accessControlQueryParam = url.searchParams.get('accesscontrol'); const folder = mockTree.find((v) => v.item.uid === uid); + if (!folder) { return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 }); } + const random = Chance(folder.item.uid); + return HttpResponse.json({ + id: random.integer({ min: 1, max: 1000 }), title: folder?.item.title, uid: folder?.item.uid, + ...additionalProperties, + ...(accessControlQueryParam ? { accessControl: mockAccessControl } : {}), }); }); diff --git a/packages/grafana-test-utils/src/handlers/api/search/handlers.ts b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts new file mode 100644 index 00000000000..195af7dac1d --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts @@ -0,0 +1,73 @@ +import { Chance } from 'chance'; +import { HttpResponse, http } from 'msw'; + +import { wellFormedTree } from '../../../fixtures/folders'; + +const [mockTree] = wellFormedTree(); + +type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>; + +const slugify = (str: string) => { + return str + .toLowerCase() + .replace(/[^\w ]+/g, '') + .replace(/ +/g, '-'); +}; + +const getLegacySearchHandler = () => + http.get('/api/search', ({ request }) => { + const folderFilter = new URL(request.url).searchParams.get('folderUIDs') || null; + const typeFilter = new URL(request.url).searchParams.get('type') || null; + // Workaround for the fixture kind being 'dashboard' instead of 'dash-db' + const mappedTypeFilter = typeFilter === 'dash-db' ? 'dashboard' : typeFilter; + const response = mockTree + .filter((filterItem) => { + const filters: FilterArray = []; + if (folderFilter && folderFilter !== 'general') { + filters.push( + ({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === folderFilter + ); + } + + if (folderFilter === 'general') { + filters.push( + ({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === undefined + ); + } + + if (mappedTypeFilter) { + filters.push(({ item }) => item.kind === mappedTypeFilter); + } + + return filters.every((filterPredicate) => filterPredicate(filterItem)); + }) + + .map(({ item }) => { + const random = Chance(item.uid); + const slugified = slugify(item.title || ''); + const parentFolder = + item.kind === 'dashboard' + ? mockTree.find((t) => t.item.kind === 'folder' && t.item.uid === item.parentUID) + : undefined; + return { + id: random.integer({ min: 1, max: 1000 }), + uid: item.uid, + orgId: 1, + title: item.title, + uri: `db/${slugified}`, + url: `/d/${item.uid}/${slugified}`, + folderUid: parentFolder?.item.uid, + folderTitle: parentFolder?.item.title, + slug: '', + type: item.kind, + tags: [], + isStarred: false, + sortMeta: 0, + isDeleted: false, + }; + }); + + return HttpResponse.json(response); + }); + +export default [getLegacySearchHandler()]; diff --git a/packages/grafana-test-utils/src/handlers/apis/folder.grafana.app/v1beta1/handlers.ts b/packages/grafana-test-utils/src/handlers/apis/folder.grafana.app/v1beta1/handlers.ts new file mode 100644 index 00000000000..3823e539ba3 --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/apis/folder.grafana.app/v1beta1/handlers.ts @@ -0,0 +1,117 @@ +import { HttpResponse, http } from 'msw'; + +import { wellFormedTree } from '../../../../fixtures/folders'; + +const [mockTree] = wellFormedTree(); + +const getFolderHandler = () => + http.get<{ folderUid: string; namespace: string }>( + '/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid', + ({ params }) => { + const { folderUid, namespace } = params; + const response = mockTree.find(({ item }) => { + return item.uid === folderUid; + }); + + if (!response) { + return HttpResponse.json( + { + kind: 'Status', + apiVersion: 'v1', + metadata: {}, + status: 'Failure', + message: 'folder not found', + code: 404, + }, + { status: 404 } + ); + } + + return HttpResponse.json({ + kind: 'Folder', + apiVersion: 'folder.grafana.app/v1beta1', + metadata: { + name: response.item.uid, + namespace, + uid: response.item.uid, + creationTimestamp: '2023-01-01T00:00:00Z', + annotations: { + // TODO: Generalise annotations in fixture data + 'grafana.app/createdBy': 'user:1', + 'grafana.app/updatedBy': 'user:2', + 'grafana.app/managedBy': 'user', + 'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z', + 'grafana.app/folder': response.item.kind === 'folder' ? response.item.parentUID : undefined, + }, + labels: { + 'grafana.app/deprecatedInternalID': '123', + }, + }, + spec: { title: response.item.title, description: '' }, + status: {}, + }); + } + ); + +const getFolderParentsHandler = () => + http.get<{ folderUid: string; namespace: string }>( + '/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid/parents', + ({ params }) => { + const { folderUid } = params; + + const folder = mockTree.find(({ item }) => { + return item.kind === 'folder' && item.uid === folderUid; + }); + if (!folder || folder.item.kind !== 'folder') { + return HttpResponse.json({ + kind: 'Status', + apiVersion: 'v1', + metadata: {}, + status: 'Failure', + message: 'folder not found', + code: 404, + }); + } + + const findParents = (parents: Array<(typeof mockTree)[number]>, folderUid?: string) => { + if (!folderUid) { + return parents; + } + + const parent = mockTree.find(({ item }) => { + return item.kind === 'folder' && item.uid === folderUid; + }); + + if (parent) { + parents.push(parent); + return findParents(parents, parent.item.kind === 'folder' ? parent.item.parentUID : undefined); + } + return parents; + }; + + const parents = findParents([], folder?.item?.parentUID); + + const mapped = parents.map((parent) => ({ + name: parent.item.uid, + title: parent.item.title, + parentUid: parent.item.kind === 'folder' ? parent.item.parentUID : undefined, + })); + + if (folder) { + mapped.push({ + name: folder.item.uid, + title: folder.item.title, + parentUid: folder.item.parentUID, + }); + } + + return HttpResponse.json({ + kind: 'FolderInfoList', + apiVersion: 'folder.grafana.app/v1beta1', + metadata: {}, + items: mapped, + }); + } + ); + +export default [getFolderHandler(), getFolderParentsHandler()]; diff --git a/packages/grafana-test-utils/src/handlers/apis/iam.grafana.app/v0alpha1/handlers.ts b/packages/grafana-test-utils/src/handlers/apis/iam.grafana.app/v0alpha1/handlers.ts new file mode 100644 index 00000000000..7a15817564b --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/apis/iam.grafana.app/v0alpha1/handlers.ts @@ -0,0 +1,29 @@ +import { HttpResponse, http } from 'msw'; + +const getDisplayMapping = () => + http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/display', ({ request }) => { + const url = new URL(request.url); + const keys = url.searchParams.getAll('key'); + + // Turn query params such as `user:1` into mock mapping of `User 1` etc. + const mockMappings = keys.map((key) => { + const [_, id] = key.split(':'); + const displayName = `User ${id}`; + return { + identity: { + type: 'user', + name: `u00000000${id}`, + }, + displayName, + internalId: parseInt(id, 10), + }; + }); + + return HttpResponse.json({ + metadata: {}, + keys, + display: mockMappings, + }); + }); + +export default [getDisplayMapping()]; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 43daedcbdb6..5b23a4fefa6 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -37,7 +37,7 @@ export const colorPickerFactory = ( pickerTriggerRef = createRef(); render() { - const { theme, children, onChange, color, id } = this.props; + const { theme, children, onChange, color } = this.props; const styles = getStyles(theme); const popoverElement = React.createElement(popover, { ...{ ...this.props, children: null }, @@ -67,7 +67,6 @@ export const colorPickerFactory = ( }) ) : ( extends ColorPickerProps, PopoverContentProps { diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx index fd015178845..b3794bf3fa9 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx @@ -66,6 +66,31 @@ export const Basic: Story = { }, }; +export const WithInfoOption: Story = { + name: 'With infoOption', + args: { + ...commonArgs, + options: [ + ...commonArgs.options, + { label: 'Can’t find your country? Select “Other” or contact an admin', value: '__INFO__', infoOption: true }, + ], + }, + render: (args) => { + const [{ value }, setArgs] = useArgs(); + + return ( + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + ); + }, +}; + export const AutoSize: Story = { args: { ...commonArgs, width: 'auto', minWidth: 20 }, render: (args) => { diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx index cce936f01f7..63240a9b0eb 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx @@ -10,7 +10,7 @@ import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils'; type Props = StandardEditorProps; // Pick a field name out of the fields -export const FieldNamePicker = ({ value, onChange, context, item, id }: Props) => { +export const FieldNamePicker = ({ value, onChange, context, item }: Props) => { const settings: FieldNamePickerConfigSettings = item.settings ?? {}; const names = useFieldDisplayNames(context.data, settings?.filter); const selectOptions = useSelectOptions(names, value, undefined, undefined, settings.baseNameMode); @@ -29,7 +29,6 @@ export const FieldNamePicker = ({ value, onChange, context, item, id }: Props) = return ( <> void; placeholder?: string; - id?: string; } -export function RefIDMultiPicker({ value, data, onChange, placeholder, id }: MultiProps) { +export function RefIDMultiPicker({ value, data, onChange, placeholder }: MultiProps) { const listOfRefIds = useMemo(() => getListOfQueryRefIds(data), [data]); const [priorSelectionState, updatePriorSelectionState] = useState<{ @@ -175,7 +172,6 @@ export function RefIDMultiPicker({ value, data, onChange, placeholder, id }: Mul } return ( { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx index 7d3e36e3f65..625ab881976 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { renderMarkdown } from '@grafana/data'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { MarkdownCellProps, TableCellStyles } from '../types'; export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) { diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index e53a1d9e827..bb3c3fea8fa 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -9,6 +9,7 @@ import { DataGridHandle, DataGridProps, RenderCellProps, + Renderers, RenderRowProps, Row, SortColumn, @@ -42,6 +43,7 @@ import { TableCellTooltip } from './components/TableCellTooltip'; import { COLUMN, TABLE } from './constants'; import { useColumnResize, + useColWidths, useFilteredRows, useFooterCalcs, useHeaderHeight, @@ -51,6 +53,7 @@ import { useSortedRows, } from './hooks'; import { + getCellActionStyles, getDefaultCellStyles, getFooterStyles, getGridStyles, @@ -58,11 +61,19 @@ import { getLinkStyles, getTooltipStyles, } from './styles'; -import { TableNGProps, TableRow, TableSummaryRow, TableColumn, InspectCellProps, TableCellStyleOptions } from './types'; +import { + TableNGProps, + TableRow, + TableSummaryRow, + TableColumn, + InspectCellProps, + TableCellStyleOptions, + FromFieldsResult, + CellRootRenderer, +} from './types'; import { applySort, canFieldBeColorized, - computeColWidths, createTypographyContext, displayJsonValue, extractPixelValue, @@ -84,7 +95,7 @@ import { withDataLinksActionsTooltip, } from './utils'; -type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; +const EXPANDED_COLUMN_KEY = 'expanded'; export function TableNG(props: TableNGProps) { const { @@ -148,6 +159,7 @@ export function TableNG(props: TableNGProps) { } = useSortedRows(filteredRows, data.fields, { hasNestedFrames, initialSortBy }); const [inspectCell, setInspectCell] = useState(null); + const [tooltipState, setTooltipState] = useState(); const [expandedRows, setExpandedRows] = useState(() => new Set()); // vt scrollbar accounting for column auto-sizing @@ -171,20 +183,9 @@ export function TableNG(props: TableNGProps) { ), [theme] ); - const widths = useMemo(() => computeColWidths(visibleFields, availableWidth), [visibleFields, availableWidth]); - const numColsFullyInView = useMemo( - () => - widths.reduce( - ([count, remainingWidth], nextWidth) => { - if (remainingWidth - nextWidth >= 0) { - return [count + 1, remainingWidth - nextWidth]; - } - return [count, 0]; - }, - [0, availableWidth] - )[0], - [widths, availableWidth] - ); + + const [widths, numFrozenColsFullyInView] = useColWidths(visibleFields, availableWidth, frozenColumns); + const headerHeight = useHeaderHeight({ columnWidths: widths, fields: visibleFields, @@ -270,15 +271,84 @@ export function TableNG(props: TableNGProps) { [enableVirtualization, resizeHandler, sortColumns, rowHeight, hasFooter, setSortColumns, onSortByChange] ); - interface Schema { - columns: TableColumn[]; - cellRootRenderers: Record; - colsWithTooltip: Record; - } + const buildNestedTableExpanderColumn = useCallback( + ( + nestedColumns: TableColumn[], + hasNestedHeaders: boolean, + renderers: Renderers + ): TableColumn => ({ + key: EXPANDED_COLUMN_KEY, + name: '', + field: { + name: '', + type: FieldType.other, + config: {}, + values: [], + }, + cellClass(row) { + if (row.__depth !== 0) { + return styles.cellNested; + } + return; + }, + colSpan(args) { + return args.type === 'ROW' && args.row.__depth === 1 ? data.fields.length : 1; + }, + renderCell: ({ row }) => { + if (row.__depth === 0) { + const rowIdx = row.__index; - const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => { - const fromFields = (f: Field[], widths: number[]) => { - const result: Schema = { + return ( + { + if (expandedRows.has(rowIdx)) { + expandedRows.delete(rowIdx); + } else { + expandedRows.add(rowIdx); + } + setExpandedRows(new Set(expandedRows)); + }} + /> + ); + } + + // Type guard to check if data exists as it's optional + const nestedData = row.data; + if (!nestedData) { + return null; + } + + const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns); + if (!expandedRecords.length) { + return ( +
+ No data +
+ ); + } + + return ( + + {...commonDataGridProps} + className={clsx(styles.grid, styles.gridNested)} + headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })} + headerRowHeight={hasNestedHeaders ? TABLE.HEADER_HEIGHT : 0} + columns={nestedColumns} + rows={expandedRecords} + renderers={renderers} + /> + ); + }, + width: COLUMN.EXPANDER_WIDTH, + minWidth: COLUMN.EXPANDER_WIDTH, + }), + [commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles] + ); + + const fromFields = useCallback( + (f: Field[], widths: number[]): FromFieldsResult => { + const result: FromFieldsResult = { columns: [], cellRootRenderers: {}, colsWithTooltip: {}, @@ -334,11 +404,7 @@ export function TableNG(props: TableNGProps) { // helps us avoid string cx and emotion per-cell const cellActionClassName = showActions - ? clsx( - 'table-cell-actions', - styles.cellActions, - justifyContent === 'flex-end' ? styles.cellActionsEnd : styles.cellActionsStart - ) + ? clsx('table-cell-actions', getCellActionStyles(theme, textAlign)) : undefined; const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field); @@ -509,13 +575,13 @@ export function TableNG(props: TableNGProps) { } } - const column: TableColumn = { + result.columns.push({ field, key: displayName, name: displayName, width, headerCellClass, - frozen: Math.min(frozenColumns, numColsFullyInView) > i, + frozen: Math.min(frozenColumns, numFrozenColsFullyInView) > i, renderCell: renderCellContent, renderHeaderCell: ({ column, sortDirection }) => ( {footerCalcs[i]}; }, - }; - - result.columns.push(column); + }); }); return result; - }; + }, + [ + applyToRowBgFn, + crossFilterOrder, + crossFilterRows, + data, + disableSanitizeHtml, + filter, + footerCalcs, + frozenColumns, + getCellActions, + isCountRowsSet, + numFrozenColsFullyInView, + onCellFilterAdded, + rowHeight, + rowHeightFn, + rows, + setFilter, + showTypeIcons, + theme, + timeRange, + ] + ); + // set up the first row's nested data and the nest field widths using useColWidths to avoid + // unnecessary re-renders on re-size. + const firstRowNestedData = useMemo( + () => (hasNestedFrames ? rows.find((r) => r.data)?.data : undefined), + [hasNestedFrames, rows] + ); + const [nestedFieldWidths] = useColWidths(firstRowNestedData?.fields ?? [], availableWidth); + + const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => { const result = fromFields(visibleFields, widths); - // handle nested frames rendering from here. - if (!hasNestedFrames) { + // if nested frames are present, augment the columns to include the nested table expander column. + if (!firstRowNestedData) { return result; } // pre-calculate renderRow and expandedColumns based on the first nested frame's fields. - const firstNestedData = rows.find((r) => r.data)?.data; - if (!firstNestedData) { - return result; - } - - const hasNestedHeaders = firstNestedData.meta?.custom?.noHeader !== true; - const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair); + const hasNestedHeaders = firstRowNestedData.meta?.custom?.noHeader !== true; + const renderRow = renderRowFactory(firstRowNestedData.fields, panelContext, expandedRows, enableSharedCrosshair); const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields( - firstNestedData.fields, - computeColWidths(firstNestedData.fields, availableWidth) + firstRowNestedData.fields, + nestedFieldWidths ); - const renderCellRoot: CellRootRenderer = (key, props) => nestedCellRootRenderers[props.column.key](key, props); - - result.cellRootRenderers.expanded = (key, props) => ; + const expanderCellRenderer: CellRootRenderer = (key, props) => ; + result.cellRootRenderers[EXPANDED_COLUMN_KEY] = expanderCellRenderer; // If we have nested frames, we need to add a column for the row expansion - result.columns.unshift({ - key: 'expanded', - name: '', - field: { - name: '', - type: FieldType.other, - config: {}, - values: [], - }, - cellClass(row) { - if (row.__depth !== 0) { - return styles.cellNested; - } - return; - }, - colSpan(args) { - return args.type === 'ROW' && args.row.__depth === 1 ? data.fields.length : 1; - }, - renderCell: ({ row }) => { - if (row.__depth === 0) { - const rowIdx = row.__index; - - return ( - { - if (expandedRows.has(rowIdx)) { - expandedRows.delete(rowIdx); - } else { - expandedRows.add(rowIdx); - } - setExpandedRows(new Set(expandedRows)); - }} - /> - ); - } - - // Type guard to check if data exists as it's optional - const nestedData = row.data; - if (!nestedData) { - return null; - } - - const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns); - if (!expandedRecords.length) { - return ( -
- No data -
- ); - } - - return ( - - {...commonDataGridProps} - className={clsx(styles.grid, styles.gridNested)} - headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })} - headerRowHeight={hasNestedHeaders ? TABLE.HEADER_HEIGHT : 0} - columns={nestedColumns} - rows={expandedRecords} - renderers={{ renderRow, renderCell: renderCellRoot }} - /> - ); - }, - width: COLUMN.EXPANDER_WIDTH, - minWidth: COLUMN.EXPANDER_WIDTH, - }); + result.columns.unshift( + buildNestedTableExpanderColumn(nestedColumns, hasNestedHeaders, { + renderRow, + renderCell: (key, props) => nestedCellRootRenderers[props.column.key](key, props), + }) + ); return result; }, [ - applyToRowBgFn, - availableWidth, - commonDataGridProps, - crossFilterOrder, - crossFilterRows, - data, - disableSanitizeHtml, + buildNestedTableExpanderColumn, enableSharedCrosshair, expandedRows, - filter, - footerCalcs, - frozenColumns, - getCellActions, - hasNestedFrames, - isCountRowsSet, - numColsFullyInView, - onCellFilterAdded, + firstRowNestedData, + fromFields, + nestedFieldWidths, panelContext, - rowHeight, - rowHeightFn, - rows, - setFilter, - showTypeIcons, - sortColumns, - styles, - timeRange, - theme, visibleFields, widths, ]); @@ -680,18 +688,16 @@ export function TableNG(props: TableNGProps) { // invalidate columns on every structureRev change. this supports width editing in the fieldConfig. // eslint-disable-next-line react-hooks/exhaustive-deps const structureRevColumns = useMemo(() => columns, [columns, structureRev]); + const renderCellRoot: CellRootRenderer = useCallback( + (key, props) => cellRootRenderers[props.column.key](key, props), + [cellRootRenderers] + ); // we need to have variables with these exact names for the localization to work properly const itemsRangeStart = pageRangeStart; const displayedEnd = pageRangeEnd; const numRows = sortedRows.length; - const renderCellRoot: CellRootRenderer = (key, props) => { - return cellRootRenderers[props.column.key](key, props); - }; - - const [tooltipState, setTooltipState] = useState(); - return ( <> diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx new file mode 100644 index 00000000000..c0a2947b770 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx @@ -0,0 +1,199 @@ +import { render, screen } from '@testing-library/react'; + +import { ActionType, Field, FieldType, HttpRequestMethod } from '@grafana/data'; + +import { MaybeWrapWithLink } from './MaybeWrapWithLink'; + +describe('MaybeWrapWithLink', () => { + describe('single link', () => { + it('renders children as a link when there is a single link', () => { + const link = { title: 'My link', url: 'http://example.com' }; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => [{ title: link.title, href: link.url, target: '_blank', origin: field }]), + config: { + links: [link], + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + const linkElement = screen.getByTitle(link.title); + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', link.url); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('does not throw if getLinks unexpectedly returns nothing when a single link is present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [{ title: 'My link', url: 'http://example.com' }], + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const childElement = screen.getByText('Test Link'); + expect(childElement).toBeInTheDocument(); + }); + }); + + describe('multi link and/or actions', () => { + it('renders a popup target link if multiple links are present', () => { + const links = [ + { title: 'My link', url: 'http://example.com' }, + { title: 'Another link', url: 'http://example.com' }, + ]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => links.map((l) => ({ title: l.title, href: l.url, target: '_blank', origin: field }))), + config: { + links, + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if multiple actions are present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [], + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + { + type: ActionType.Fetch, + title: 'Another action', + [ActionType.Fetch]: { method: HttpRequestMethod.POST, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if a single action is present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [], + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if a mixture of actions and links are present', () => { + const links = [{ title: 'My link', url: 'http://example.com' }]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => links.map((l) => ({ title: l.title, href: l.url, target: '_blank', origin: field }))), + config: { + links, + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + }); + + describe('no links or actions', () => { + it('passes the children through when no links or actions are present', () => { + const links = [ + { title: 'My link', url: 'http://example.com' }, + { title: 'Another link', url: 'http://example.com' }, + ]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links, + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const childElement = screen.getByText('Test Link'); + expect(childElement).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx similarity index 69% rename from packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx index ed8e3c88a8d..b43728dd1e8 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx @@ -1,10 +1,10 @@ import { memo, ReactNode } from 'react'; import { Field } from '@grafana/data'; +import { t } from '@grafana/i18n'; -import { renderSingleLink } from '../DataLinksActionsTooltip'; - -import { getCellLinks } from './utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { getCellLinks } from '../utils'; interface MaybeWrapWithLinkProps { field: Field; @@ -23,8 +23,12 @@ export const MaybeWrapWithLink = memo(({ field, rowIdx, children }: MaybeWrapWit } // as faux link that acts as hit-area for tooltip activation else if (linksCount + actionsCount > 0) { - // eslint-disable-next-line jsx-a11y/anchor-is-valid - return {children}; + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + {children} + + ); } // raw value diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts index ad06992984e..a45dfc12125 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts @@ -477,7 +477,7 @@ describe('TableNG hooks', () => { }), columnWidths: [100, 100, 100], enabled: true, - typographyCtx: { ...typographyCtx, avgCharWidth: 5, measureHeight: jest.fn(() => 38) }, + typographyCtx: { ...typographyCtx, avgCharWidth: 5, measureHeight: jest.fn(() => 44) }, sortColumns: [], }); }); @@ -518,7 +518,7 @@ describe('TableNG hooks', () => { }); }); - expect(heightFn).toHaveBeenCalledWith('Longer name that needs wrapping', 86, modifiedFields[0], -1, 6); + expect(heightFn).toHaveBeenCalledWith('Longer name that needs wrapping', 86, modifiedFields[0], -1, 22); modifiedFields = fields.map((field) => { if (field.name === 'name') { @@ -549,7 +549,21 @@ describe('TableNG hooks', () => { }); }); - expect(heightFn).toHaveBeenCalledWith('Longer name that needs wrapping', 26, modifiedFields[0], -1, 6); + expect(heightFn).toHaveBeenCalledWith('Longer name that needs wrapping', 26, modifiedFields[0], -1, 22); + }); + + it('does not throw if a field has been deleted but the colWidth has not yet been updated', () => { + const { fields } = setupData(); + const { result } = renderHook(() => { + return useHeaderHeight({ + fields, + columnWidths: [100, 100, 100, 100], + enabled: true, + typographyCtx, + sortColumns: [], + }); + }); + expect(result.current).toBe(28); }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 5162aac82f8..7a1be693d8c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -1,7 +1,14 @@ -import { useState, useMemo, useCallback, useRef, useLayoutEffect, RefObject, CSSProperties } from 'react'; +import { useState, useMemo, useCallback, useRef, useLayoutEffect, RefObject, CSSProperties, useEffect } from 'react'; import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; -import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; +import { + compareArrayValues, + Field, + fieldReducers, + FieldType, + formattedValueToString, + reduceField, +} from '@grafana/data'; import { TableColumnResizeActionCallback } from '../types'; @@ -13,6 +20,7 @@ import { applySort, getColumnTypes, getRowHeight, + computeColWidths, buildHeaderHeightMeasurers, buildCellHeightMeasurers, } from './utils'; @@ -183,16 +191,17 @@ export function usePaginatedRows( return rows.slice(0, 100).reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0); }, [rows, rowHeight, enabled]); + const smallPagination = useMemo(() => enabled && width < TABLE.PAGINATION_LIMIT, [enabled, width]); + // using dimensions of the panel, calculate pagination parameters - const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd, smallPagination } = useMemo((): { + const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd } = useMemo((): { numPages: number; rowsPerPage: number; pageRangeStart: number; pageRangeEnd: number; - smallPagination: boolean; } => { if (!enabled) { - return { numPages: 0, rowsPerPage: 0, pageRangeStart: 1, pageRangeEnd: numRows, smallPagination: false }; + return { numPages: 0, rowsPerPage: 0, pageRangeStart: 1, pageRangeEnd: numRows }; } // calculate number of rowsPerPage based on height stack @@ -207,16 +216,15 @@ export function usePaginatedRows( if (pageRangeEnd > numRows) { pageRangeEnd = numRows; } - const smallPagination = width < TABLE.PAGINATION_LIMIT; + const numPages = Math.ceil(numRows / rowsPerPage); return { numPages, rowsPerPage, pageRangeStart, pageRangeEnd, - smallPagination, }; - }, [width, height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]); + }, [height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]); // safeguard against page overflow on panel resize or other factors useLayoutEffect(() => { @@ -345,13 +353,19 @@ export function useHeaderHeight({ const columnAvailableWidths = useMemo( () => columnWidths.map((c, idx) => { + if (idx >= fields.length) { + return 0; // no width available for this column yet + } + let width = c - 2 * TABLE.CELL_PADDING - TABLE.BORDER_RIGHT; + const field = fields[idx]; + // filtering icon - if (fields[idx]?.config?.custom?.filterable) { + if (field.config?.custom?.filterable) { width -= perIconSpace; } // sorting icon - if (sortColumns.some((col) => col.columnKey === getDisplayName(fields[idx]))) { + if (sortColumns.some((col) => col.columnKey === getDisplayName(field))) { width -= perIconSpace; } // type icon @@ -368,7 +382,15 @@ export function useHeaderHeight({ if (!enabled) { return 0; } - return getRowHeight(fields, -1, columnAvailableWidths, TABLE.HEADER_HEIGHT, measurers, TABLE.CELL_PADDING); + return getRowHeight( + fields, + -1, + columnAvailableWidths, + TABLE.HEADER_HEIGHT, + measurers, + TABLE.LINE_HEIGHT, + TABLE.CELL_PADDING + ); }, [fields, enabled, columnAvailableWidths, measurers]); return headerHeight; @@ -534,3 +556,45 @@ export function useScrollbarWidth(ref: RefObject, height: number return scrollbarWidth; } + +const numIsEqual = (a: number, b: number) => a === b; + +export function useColWidths( + visibleFields: Field[], + availableWidth: number, + frozenColumns?: number +): [number[], number] { + const [widths, setWidths] = useState(computeColWidths(visibleFields, availableWidth)); + + // only replace the widths array if something actually changed + useEffect(() => { + const newWidths = computeColWidths(visibleFields, availableWidth); + if (!compareArrayValues(widths, newWidths, numIsEqual)) { + setWidths(newWidths); + } + }, [availableWidth, widths, visibleFields]); + + // this is to avoid buggy situations where all visible columns are frozen + const numFrozenColsFullyInView = useMemo(() => { + if (!frozenColumns || frozenColumns <= 0) { + return -1; + } + + const fullyVisibleCols = widths.reduce( + ([count, remainingWidth], nextWidth) => { + if (remainingWidth - nextWidth >= 0) { + return [count + 1, remainingWidth - nextWidth]; + } + return [count, 0]; + }, + [0, availableWidth] + )[0]; + + // de-noise memoized changes to the columns array, and only change this + // number when the number of frozen columns changes or once there are fewer + // visible columns than the number of frozen columns. + return Math.min(fullyVisibleCols, frozenColumns); + }, [widths, availableWidth, frozenColumns]); + + return [widths, numFrozenColsFullyInView]; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/styles.ts b/packages/grafana-ui/src/components/Table/TableNG/styles.ts index b21bb702041..1094108a587 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/styles.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/styles.ts @@ -88,19 +88,6 @@ export const getGridStyles = (theme: GrafanaTheme2, enablePagination?: boolean, color: theme.colors.text.secondary, fontSize: theme.typography.h4.fontSize, }), - cellActions: css({ - display: 'none', - position: 'absolute', - top: 0, - margin: 'auto', - height: '100%', - color: theme.colors.text.primary, - background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', - padding: theme.spacing.x0_5, - paddingInlineStart: theme.spacing.x1, - }), - cellActionsEnd: css({ left: 0 }), - cellActionsStart: css({ right: 0 }), headerRow: css({ paddingBlockStart: 0, fontWeight: 'normal', @@ -158,6 +145,20 @@ export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, should }, }); +export const getCellActionStyles = (theme: GrafanaTheme2, textAlign: TextAlign) => + css({ + display: 'none', + position: 'absolute', + top: 0, + margin: 'auto', + height: '100%', + color: theme.colors.text.primary, + background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', + padding: theme.spacing.x0_5, + paddingInlineStart: theme.spacing.x1, + [textAlign === 'right' ? 'left' : 'right']: 0, + }); + export const getLinkStyles = (theme: GrafanaTheme2, canBeColorized: boolean) => css({ a: { diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 0f1883456a7..efdd9495336 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -1,5 +1,5 @@ import { FC, SyntheticEvent } from 'react'; -import { Column } from 'react-data-grid'; +import { CellRendererProps, Column } from 'react-data-grid'; import { DataFrame, @@ -306,3 +306,11 @@ export interface MeasureCellHeightEntry { */ fieldIdxs: number[]; } + +export type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; + +export interface FromFieldsResult { + columns: TableColumn[]; + cellRootRenderers: Record; + colsWithTooltip: Record; +} diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index dff45cb2a18..2709bfa067c 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -9,7 +9,6 @@ export interface UnitPickerProps { onChange: (item?: string) => void; value?: string; width?: number; - id?: string; } function formatCreateLabel(input: string) { @@ -22,7 +21,7 @@ export class UnitPicker extends PureComponent { }; render() { - const { value, width, id } = this.props; + const { value, width } = this.props; // Set the current selection let current: SelectableValue | undefined = undefined; @@ -57,7 +56,6 @@ export class UnitPicker extends PureComponent { return ( 0 { - repo, err := rc.repoGetter.AsRepository(ctx, obj) + repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj) if err != nil { logger.Warn("unable to get repository for cleanup") } else { @@ -438,7 +438,7 @@ func (rc *RepositoryController) process(item *queueItem) error { return nil } - repo, err := rc.repoGetter.AsRepository(ctx, obj) + repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj) if err != nil { return fmt.Errorf("unable to create repository from configuration: %w", err) } diff --git a/pkg/registry/apis/provisioning/extra.go b/pkg/registry/apis/provisioning/extra.go index e7f888a914b..b1ab565d1e3 100644 --- a/pkg/registry/apis/provisioning/extra.go +++ b/pkg/registry/apis/provisioning/extra.go @@ -3,13 +3,14 @@ package provisioning import ( "context" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kube-openapi/pkg/spec3" + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "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/repository" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/kube-openapi/pkg/spec3" ) type Extra interface { @@ -17,7 +18,7 @@ type Extra interface { UpdateStorage(storage map[string]rest.Storage) error PostProcessOpenAPI(oas *spec3.OpenAPI) error GetJobWorkers() []jobs.Worker - AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) + AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error) RepositoryTypes() []provisioning.RepositoryType Mutators() []controller.Mutator } diff --git a/pkg/registry/apis/provisioning/jobs.go b/pkg/registry/apis/provisioning/jobs.go index 8c352bbeee1..a1fc5d7648b 100644 --- a/pkg/registry/apis/provisioning/jobs.go +++ b/pkg/registry/apis/provisioning/jobs.go @@ -18,7 +18,7 @@ import ( type jobsConnector struct { repoGetter RepoGetter jobs jobs.Queue - historic jobs.History + historic jobs.HistoryReader } func (*jobsConnector) New() runtime.Object { diff --git a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go index 4f508c25a8d..0504fc414d5 100644 --- a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go +++ b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go @@ -18,7 +18,7 @@ type ConcurrentJobDriver struct { leaseRenewalInterval time.Duration store Store repoGetter RepoGetter - historicJobs History + historicJobs HistoryWriter workers []Worker notifications chan struct{} } @@ -29,7 +29,7 @@ func NewConcurrentJobDriver( jobTimeout, cleanupInterval, jobInterval, leaseRenewalInterval time.Duration, store Store, repoGetter RepoGetter, - historicJobs History, + historicJobs HistoryWriter, notifications chan struct{}, workers ...Worker, ) (*ConcurrentJobDriver, error) { diff --git a/pkg/registry/apis/provisioning/jobs/driver.go b/pkg/registry/apis/provisioning/jobs/driver.go index f64eebe3528..cd186f3151f 100644 --- a/pkg/registry/apis/provisioning/jobs/driver.go +++ b/pkg/registry/apis/provisioning/jobs/driver.go @@ -68,7 +68,7 @@ type jobDriver struct { repoGetter RepoGetter // save info about finished jobs - historicJobs History + historicJobs HistoryWriter // Workers process the job. // Only the first worker who supports the job will process it; the rest are ignored. @@ -82,7 +82,7 @@ func NewJobDriver( jobTimeout, jobInterval, leaseRenewalInterval time.Duration, store Store, repoGetter RepoGetter, - historicJobs History, + historicJobs HistoryWriter, notifications chan struct{}, workers ...Worker, ) (*jobDriver, error) { diff --git a/pkg/registry/apis/provisioning/jobs/history_mock.go b/pkg/registry/apis/provisioning/jobs/history_mock.go deleted file mode 100644 index f6697d33bcf..00000000000 --- a/pkg/registry/apis/provisioning/jobs/history_mock.go +++ /dev/null @@ -1,205 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package jobs - -import ( - context "context" - - v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - mock "github.com/stretchr/testify/mock" -) - -// MockHistory is an autogenerated mock type for the History type -type MockHistory struct { - mock.Mock -} - -type MockHistory_Expecter struct { - mock *mock.Mock -} - -func (_m *MockHistory) EXPECT() *MockHistory_Expecter { - return &MockHistory_Expecter{mock: &_m.Mock} -} - -// GetJob provides a mock function with given fields: ctx, namespace, repo, uid -func (_m *MockHistory) GetJob(ctx context.Context, namespace string, repo string, uid string) (*v0alpha1.Job, error) { - ret := _m.Called(ctx, namespace, repo, uid) - - if len(ret) == 0 { - panic("no return value specified for GetJob") - } - - var r0 *v0alpha1.Job - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (*v0alpha1.Job, error)); ok { - return rf(ctx, namespace, repo, uid) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *v0alpha1.Job); ok { - r0 = rf(ctx, namespace, repo, uid) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v0alpha1.Job) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, namespace, repo, uid) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockHistory_GetJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJob' -type MockHistory_GetJob_Call struct { - *mock.Call -} - -// GetJob is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - repo string -// - uid string -func (_e *MockHistory_Expecter) GetJob(ctx interface{}, namespace interface{}, repo interface{}, uid interface{}) *MockHistory_GetJob_Call { - return &MockHistory_GetJob_Call{Call: _e.mock.On("GetJob", ctx, namespace, repo, uid)} -} - -func (_c *MockHistory_GetJob_Call) Run(run func(ctx context.Context, namespace string, repo string, uid string)) *MockHistory_GetJob_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockHistory_GetJob_Call) Return(_a0 *v0alpha1.Job, _a1 error) *MockHistory_GetJob_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockHistory_GetJob_Call) RunAndReturn(run func(context.Context, string, string, string) (*v0alpha1.Job, error)) *MockHistory_GetJob_Call { - _c.Call.Return(run) - return _c -} - -// RecentJobs provides a mock function with given fields: ctx, namespace, repo -func (_m *MockHistory) RecentJobs(ctx context.Context, namespace string, repo string) (*v0alpha1.JobList, error) { - ret := _m.Called(ctx, namespace, repo) - - if len(ret) == 0 { - panic("no return value specified for RecentJobs") - } - - var r0 *v0alpha1.JobList - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v0alpha1.JobList, error)); ok { - return rf(ctx, namespace, repo) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *v0alpha1.JobList); ok { - r0 = rf(ctx, namespace, repo) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v0alpha1.JobList) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, namespace, repo) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockHistory_RecentJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecentJobs' -type MockHistory_RecentJobs_Call struct { - *mock.Call -} - -// RecentJobs is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - repo string -func (_e *MockHistory_Expecter) RecentJobs(ctx interface{}, namespace interface{}, repo interface{}) *MockHistory_RecentJobs_Call { - return &MockHistory_RecentJobs_Call{Call: _e.mock.On("RecentJobs", ctx, namespace, repo)} -} - -func (_c *MockHistory_RecentJobs_Call) Run(run func(ctx context.Context, namespace string, repo string)) *MockHistory_RecentJobs_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *MockHistory_RecentJobs_Call) Return(_a0 *v0alpha1.JobList, _a1 error) *MockHistory_RecentJobs_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockHistory_RecentJobs_Call) RunAndReturn(run func(context.Context, string, string) (*v0alpha1.JobList, error)) *MockHistory_RecentJobs_Call { - _c.Call.Return(run) - return _c -} - -// WriteJob provides a mock function with given fields: ctx, job -func (_m *MockHistory) WriteJob(ctx context.Context, job *v0alpha1.Job) error { - ret := _m.Called(ctx, job) - - if len(ret) == 0 { - panic("no return value specified for WriteJob") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Job) error); ok { - r0 = rf(ctx, job) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockHistory_WriteJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteJob' -type MockHistory_WriteJob_Call struct { - *mock.Call -} - -// WriteJob is a helper method to define mock.On call -// - ctx context.Context -// - job *v0alpha1.Job -func (_e *MockHistory_Expecter) WriteJob(ctx interface{}, job interface{}) *MockHistory_WriteJob_Call { - return &MockHistory_WriteJob_Call{Call: _e.mock.On("WriteJob", ctx, job)} -} - -func (_c *MockHistory_WriteJob_Call) Run(run func(ctx context.Context, job *v0alpha1.Job)) *MockHistory_WriteJob_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v0alpha1.Job)) - }) - return _c -} - -func (_c *MockHistory_WriteJob_Call) Return(_a0 error) *MockHistory_WriteJob_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockHistory_WriteJob_Call) RunAndReturn(run func(context.Context, *v0alpha1.Job) error) *MockHistory_WriteJob_Call { - _c.Call.Return(run) - return _c -} - -// NewMockHistory creates a new instance of MockHistory. 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 NewMockHistory(t interface { - mock.TestingT - Cleanup(func()) -}) *MockHistory { - mock := &MockHistory{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/history.go b/pkg/registry/apis/provisioning/jobs/history_reader.go similarity index 63% rename from pkg/registry/apis/provisioning/jobs/history.go rename to pkg/registry/apis/provisioning/jobs/history_reader.go index 95fc4b73b3d..ac54ffe8c69 100644 --- a/pkg/registry/apis/provisioning/jobs/history.go +++ b/pkg/registry/apis/provisioning/jobs/history_reader.go @@ -6,7 +6,6 @@ import ( 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/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" @@ -14,13 +13,10 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" ) -// History keeps track of completed jobs +// HistoryReader keeps track of completed jobs // -//go:generate mockery --name History --structname MockHistory --inpackage --filename history_mock.go --with-expecter -type History interface { - // Adds a job to the history - WriteJob(ctx context.Context, job *provisioning.Job) error - +//go:generate mockery --name HistoryReader --structname MockHistoryReader --inpackage --filename history_reader_mock.go --with-expecter +type HistoryReader interface { // Gets recent jobs for a repository RecentJobs(ctx context.Context, namespace, repo string) (*provisioning.JobList, error) @@ -30,14 +26,9 @@ type History interface { // NewStorageBackedHistory creates a History client backed by unified storage // This should be replaced by loki when running in cloud -func NewStorageBackedHistory(store rest.Storage) (History, error) { +func NewStorageBackedHistory(store rest.Storage) (HistoryReader, error) { var ok bool history := &storageBackedHistory{} - history.creator, ok = store.(rest.Creater) - if !ok { - return nil, fmt.Errorf("storage does not implement rest.Creater") - } - history.lister, ok = store.(rest.Lister) if !ok { return nil, fmt.Errorf("storage does not implement rest.Lister") @@ -47,35 +38,7 @@ func NewStorageBackedHistory(store rest.Storage) (History, error) { } type storageBackedHistory struct { - creator rest.Creater - lister rest.Lister -} - -// Write implements History. -func (s *storageBackedHistory) WriteJob(ctx context.Context, job *provisioning.Job) error { - if job.UID == "" { - return fmt.Errorf("missing UID in job '%s'", job.GetName()) - } - if job.Labels == nil { - job.Labels = make(map[string]string) - } - job.Labels[LabelRepository] = job.Spec.Repository - job.Labels[LabelJobOriginalUID] = string(job.UID) - - // Generate a new name based on the input job - job.GenerateName = job.Name + "-" - job.Name = "" - // We also reset the UID as this is not the same object. - job.UID = "" - // We aren't allowed to write with ResourceVersion set. - job.ResourceVersion = "" - - _, err := s.creator.Create(ctx, &provisioning.HistoricJob{ - ObjectMeta: job.ObjectMeta, - Spec: job.Spec, - Status: job.Status, - }, nil, &metav1.CreateOptions{}) - return err + lister rest.Lister } func (s *storageBackedHistory) getJobs(ctx context.Context, namespace string, labels labels.Set) (*provisioning.JobList, error) { diff --git a/pkg/registry/apis/provisioning/jobs/history_writer.go b/pkg/registry/apis/provisioning/jobs/history_writer.go new file mode 100644 index 00000000000..67a9b427006 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/history_writer.go @@ -0,0 +1,68 @@ +package jobs + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" +) + +// HistoryWriter stores completed jobs +// +//go:generate mockery --name=HistoryWriter --structname=MockHistoryWriter --inpackage --filename history_writer_mock.go --with-expecter +type HistoryWriter interface { + // Adds a job to the history + WriteJob(ctx context.Context, job *provisioning.Job) error +} + +type apiClientHistoryWriter struct { + client client.ProvisioningV0alpha1Interface +} + +// NewAPIClientHistoryWriter creates a HistoryWriter backed by the provisioning API client. +func NewAPIClientHistoryWriter(provisioningClient client.ProvisioningV0alpha1Interface) HistoryWriter { + return &apiClientHistoryWriter{ + client: provisioningClient, + } +} + +// WriteJob implements HistoryWriter. +func (w *apiClientHistoryWriter) WriteJob(ctx context.Context, job *provisioning.Job) error { + if job.UID == "" { + return fmt.Errorf("missing UID in job '%s'", job.GetName()) + } + + // Create a copy of the job's metadata to avoid modifying the original + meta := job.ObjectMeta.DeepCopy() + + // Ensure labels map exists + if meta.Labels == nil { + meta.Labels = make(map[string]string) + } + + // Add required labels for history tracking + meta.Labels[LabelRepository] = job.Spec.Repository + meta.Labels[LabelJobOriginalUID] = string(job.UID) + + // Generate a new name based on the input job + meta.GenerateName = job.Name + "-" + meta.Name = "" + // We also reset the UID as this is not the same object. + meta.UID = "" + // We aren't allowed to write with ResourceVersion set. + meta.ResourceVersion = "" + + // Create the historic job using the API client + historicJob := &provisioning.HistoricJob{ + ObjectMeta: *meta, + Spec: job.Spec, + Status: job.Status, + } + + _, err := w.client.HistoricJobs(job.Namespace).Create(ctx, historicJob, metav1.CreateOptions{}) + + return err +} diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index d4bd0d21825..e8b834bd255 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -34,6 +34,7 @@ import ( client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" informers "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions" listers "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1" + commonMeta "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" apiutils "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apiserver/readonly" @@ -56,7 +57,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/usage" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/services/apiserver" @@ -98,20 +98,19 @@ type APIBuilder struct { jobs.Queue jobs.Store } - jobHistoryConfig *JobHistoryConfig - jobHistory jobs.History - resourceLister resources.ResourceLister - repositoryLister listers.RepositoryLister - legacyMigrator legacy.LegacyMigrator - storageStatus dualwrite.Service - unified resource.ResourceClient - decryptSvc secret.DecryptService - repositorySecrets secrets.RepositorySecrets // << Will be removed when the decryptSvc usage is stable - client client.ProvisioningV0alpha1Interface - access authlib.AccessChecker - mutators []controller.Mutator - statusPatcher *controller.RepositoryStatusPatcher - healthChecker *controller.HealthChecker + jobHistoryConfig *JobHistoryConfig + jobHistoryLoki *jobs.LokiJobHistory + resourceLister resources.ResourceLister + repositoryLister listers.RepositoryLister + legacyMigrator legacy.LegacyMigrator + storageStatus dualwrite.Service + unified resource.ResourceClient + decrypter repository.Decrypter + client client.ProvisioningV0alpha1Interface + access authlib.AccessChecker + mutators []controller.Mutator + statusPatcher *controller.RepositoryStatusPatcher + healthChecker *controller.HealthChecker // Extras provides additional functionality to the API. extras []Extra availableRepositoryTypes map[provisioning.RepositoryType]bool @@ -130,7 +129,6 @@ func NewAPIBuilder( storageStatus dualwrite.Service, usageStats usagestats.Service, decryptSvc secret.DecryptService, - repositorySecrets secrets.RepositorySecrets, access authlib.AccessChecker, tracer tracing.Tracer, extraBuilders []ExtraBuilder, @@ -141,8 +139,8 @@ func NewAPIBuilder( resourceLister := resources.NewResourceLister(unified, unified, legacyMigrator, storageStatus) mutators := []controller.Mutator{ - git.Mutator(repositorySecrets), - github.Mutator(repositorySecrets), + git.Mutator(), + github.Mutator(), } b := &APIBuilder{ @@ -159,8 +157,7 @@ func NewAPIBuilder( legacyMigrator: legacyMigrator, storageStatus: storageStatus, unified: unified, - decryptSvc: decryptSvc, - repositorySecrets: repositorySecrets, + decrypter: repository.DecryptService(decryptSvc), access: access, jobHistoryConfig: jobHistoryConfig, availableRepositoryTypes: map[provisioning.RepositoryType]bool{ @@ -233,7 +230,6 @@ func RegisterAPIService( storageStatus dualwrite.Service, usageStats usagestats.Service, decryptSvc secret.DecryptService, - repositorySecrets secrets.RepositorySecrets, tracer tracing.Tracer, extraBuilders []ExtraBuilder, ) (*APIBuilder, error) { @@ -251,7 +247,6 @@ func RegisterAPIService( legacyMigrator, storageStatus, usageStats, decryptSvc, - repositorySecrets, access, tracer, extraBuilders, @@ -441,28 +436,27 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI storage := map[string]rest.Storage{} // Create job history based on configuration - // Default to in-memory cache if no config provided - var jobHistory jobs.History + // Default to unified storage if no config provided + var jobHistory jobs.HistoryReader if b.jobHistoryConfig != nil && b.jobHistoryConfig.Loki != nil { - jobHistory = jobs.NewLokiJobHistory(*b.jobHistoryConfig.Loki) + b.jobHistoryLoki = jobs.NewLokiJobHistory(*b.jobHistoryConfig.Loki) + jobHistory = b.jobHistoryLoki } else { historicJobStore, err := grafanaregistry.NewCompleteRegistryStore(opts.Scheme, provisioning.HistoricJobResourceInfo, opts.OptsGetter) if err != nil { - return fmt.Errorf("failed to create historic job storage: %w", err) + return fmt.Errorf("create historic job storage: %w", err) } jobHistory, err = jobs.NewStorageBackedHistory(historicJobStore) if err != nil { - return fmt.Errorf("failed to create historic job wrapper: %w", err) + return fmt.Errorf("create historic job wrapper: %w", err) } - storage[provisioning.HistoricJobResourceInfo.StoragePath()] = historicJobStore } - b.jobHistory = jobHistory b.jobs, err = jobs.NewJobStore(realJobStore, 30*time.Second) // FIXME: this timeout if err != nil { - return fmt.Errorf("failed to create job store: %w", err) + return fmt.Errorf("create job store: %w", err) } // Although we never interact with jobs via the API, we want them to be readable (watchable!) from the API. @@ -485,7 +479,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI storage[provisioning.RepositoryResourceInfo.StoragePath("jobs")] = &jobsConnector{ repoGetter: b, jobs: b.jobs, - historic: b.jobHistory, + historic: jobHistory, } // Add any extra storage @@ -507,6 +501,12 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis return nil // This is normal for sub-resource } + // FIXME: Do nothing for HistoryJobs for now + _, ok := obj.(*provisioning.HistoricJob) + if ok { + return nil + } + r, ok := obj.(*provisioning.Repository) if !ok { return fmt.Errorf("expected repository configuration") @@ -551,7 +551,13 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm return nil } - repo, err := b.asRepository(ctx, obj) + // FIXME: Do nothing for HistoryJobs for now + _, ok := obj.(*provisioning.HistoricJob) + if ok { + return nil + } + + repo, err := b.asRepository(ctx, obj, a.GetOldObject()) if err != nil { return err } @@ -560,7 +566,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm cfg := repo.Config() if a.GetOperation() == admission.Update { - oldRepo, err := b.asRepository(ctx, a.GetOldObject()) + oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil) if err != nil { return fmt.Errorf("get old repository for update: %w", err) } @@ -743,6 +749,13 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH workers = append(workers, extra.GetJobWorkers()...) } + var jobHistoryWriter jobs.HistoryWriter + if b.jobHistoryLoki != nil { + jobHistoryWriter = b.jobHistoryLoki + } else { + jobHistoryWriter = jobs.NewAPIClientHistoryWriter(b.GetClient()) + } + // This is basically our own JobQueue system driver, err := jobs.NewConcurrentJobDriver( 3, // 3 drivers for now @@ -750,7 +763,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH time.Minute, // Cleanup jobs 30*time.Second, // Periodically look for new jobs 30*time.Second, // Lease renewal interval - b.jobs, b, b.jobHistory, + b.jobs, b, jobHistoryWriter, jobController.InsertNotifications(), workers..., ) @@ -783,8 +796,8 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH go repoController.Run(postStartHookCtx.Context, repoControllerWorkers) - // If Loki not used, start the controller for history jobs - if b.jobHistoryConfig == nil || b.jobHistoryConfig.Loki == nil { + // If Loki not used, initialize the API client-based history writer and start the controller for history jobs + if b.jobHistoryLoki == nil { // Create HistoryJobController for cleanup of old job history entries // Separate informer factory for HistoryJob cleanup with resync interval historyJobExpiration := 30 * time.Second @@ -1245,7 +1258,7 @@ func (b *APIBuilder) GetRepository(ctx context.Context, name string) (repository if err != nil { return nil, err } - return b.asRepository(ctx, obj) + return b.asRepository(ctx, obj, nil) } func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (repository.Repository, error) { @@ -1265,7 +1278,7 @@ func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (rep return repo, err } -func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repository.Repository, error) { +func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object, old runtime.Object) (repository.Repository, error) { if obj == nil { return nil, fmt.Errorf("missing repository object") } @@ -1273,13 +1286,30 @@ func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repo if !ok { return nil, fmt.Errorf("expected repository configuration") } - return b.AsRepository(ctx, r) + + // Copy previous values if they exist + if old != nil { + o, ok := old.(*provisioning.Repository) + if ok && !o.Secure.IsZero() { + if r.Secure.Token.IsZero() { + r.Secure.Token = o.Secure.Token + } + if r.Secure.WebhookSecret.IsZero() { + r.Secure.WebhookSecret = o.Secure.WebhookSecret + } + } + } + + return b.RepositoryFromConfig(ctx, r) } -func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) { +func (b *APIBuilder) RepositoryFromConfig(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) { + // Prepare a decrypter + secure := b.decrypter(r) + // Try first with any extra for _, extra := range b.extras { - r, err := extra.AsRepository(ctx, r) + r, err := extra.AsRepository(ctx, r, secure) if err != nil { return nil, fmt.Errorf("convert repository for extra %T: %w", extra, err) } @@ -1289,6 +1319,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor } } + var token commonMeta.RawSecureValue + if r.Secure.Token.IsZero() { + t, err := secure.Token(ctx) + if err != nil { + return nil, fmt.Errorf("unable to decrypt token: %w", err) + } + token = t + } + switch r.Spec.Type { case provisioning.BitbucketRepositoryType: return nil, errors.New("repository type bitbucket is not available") @@ -1297,26 +1336,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor case provisioning.LocalRepositoryType: return local.NewLocal(r, b.localFileResolver), nil case provisioning.GitRepositoryType: - // Decrypt token if needed - token := r.Spec.Git.Token - if token == "" && len(r.Spec.Git.EncryptedToken) > 0 { - decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(r.Spec.Git.EncryptedToken)) - if err != nil { - return nil, fmt.Errorf("decrypt git token: %w", err) - } - token = string(decrypted) - } - cfg := git.RepositoryConfig{ - URL: r.Spec.Git.URL, - Branch: r.Spec.Git.Branch, - Path: r.Spec.Git.Path, - TokenUser: r.Spec.Git.TokenUser, - Token: token, - EncryptedToken: r.Spec.Git.EncryptedToken, + URL: r.Spec.Git.URL, + Branch: r.Spec.Git.Branch, + Path: r.Spec.Git.Path, + TokenUser: r.Spec.Git.TokenUser, + Token: token, } - return git.NewGitRepository(ctx, r, cfg, b.repositorySecrets) + return git.NewGitRepository(ctx, r, cfg) case provisioning.GitHubRepositoryType: logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path) logger.Info("Instantiating Github repository") @@ -1326,30 +1354,19 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor return nil, fmt.Errorf("github configuration is required for nano git") } - // Decrypt GitHub token if needed - ghToken := ghCfg.Token - if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { - decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) - if err != nil { - return nil, fmt.Errorf("decrypt github token: %w", err) - } - ghToken = string(decrypted) - } - gitCfg := git.RepositoryConfig{ - URL: ghCfg.URL, - Branch: ghCfg.Branch, - Path: ghCfg.Path, - Token: ghToken, - EncryptedToken: ghCfg.EncryptedToken, + URL: ghCfg.URL, + Branch: ghCfg.Branch, + Path: ghCfg.Path, + Token: token, } - gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, b.repositorySecrets) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg) if err != nil { return nil, fmt.Errorf("error creating git repository: %w", err) } - ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken, b.repositorySecrets) + ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, token) if err != nil { return nil, fmt.Errorf("error creating github repository: %w", err) } diff --git a/pkg/registry/apis/provisioning/repository/git/git.go b/pkg/registry/apis/provisioning/repository/git/git.go index e7ef7ca4b38..02c0f70dc9b 100644 --- a/pkg/registry/apis/provisioning/repository/git/git.go +++ b/pkg/registry/apis/provisioning/repository/git/git.go @@ -12,7 +12,6 @@ type GitRepository interface { repository.Writer repository.Reader repository.StageableRepository - repository.Hooks URL() string Branch() string } diff --git a/pkg/registry/apis/provisioning/repository/git/mutator.go b/pkg/registry/apis/provisioning/repository/git/mutator.go index c4eded27f44..44b5535c4ed 100644 --- a/pkg/registry/apis/provisioning/repository/git/mutator.go +++ b/pkg/registry/apis/provisioning/repository/git/mutator.go @@ -9,10 +9,9 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" ) -func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { +func Mutator() controller.Mutator { return func(ctx context.Context, obj runtime.Object) error { repo, ok := obj.(*provisioning.Repository) if !ok { @@ -40,16 +39,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { } } - if repo.Spec.Git.Token != "" { - secretName := repo.Name + gitTokenSecretSuffix - nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.Git.Token) - if err != nil { - return err - } - repo.Spec.Git.EncryptedToken = nameOrValue - repo.Spec.Git.Token = "" - } - return nil } } diff --git a/pkg/registry/apis/provisioning/repository/git/mutator_test.go b/pkg/registry/apis/provisioning/repository/git/mutator_test.go index de52ab0c0f6..e874cd304f9 100644 --- a/pkg/registry/apis/provisioning/repository/git/mutator_test.go +++ b/pkg/registry/apis/provisioning/repository/git/mutator_test.go @@ -2,98 +2,23 @@ package git import ( "context" - "errors" "testing" - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" ) func TestMutator(t *testing.T) { tests := []struct { - name string - obj runtime.Object - token string - setupMocks func(*secrets.MockRepositorySecrets) - expectedToken string - expectedEncryptedToken string - expectedError string - expectedURL string + name string + obj runtime.Object + token string + expectedError string + expectedURL string }{ - { - name: "successful token encryption", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitRepositoryType, - Git: &provisioning.GitRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitRepositoryType, - Git: &provisioning.GitRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - "test-repo"+gitTokenSecretSuffix, - "secret-token", - ).Return([]byte("encrypted-token"), nil) - }, - expectedToken: "", - expectedEncryptedToken: "encrypted-token", - }, - { - name: "encryption error", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitRepositoryType, - Git: &provisioning.GitRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitRepositoryType, - Git: &provisioning.GitRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - "test-repo"+gitTokenSecretSuffix, - "secret-token", - ).Return(nil, errors.New("encryption failed")) - }, - expectedError: "encryption failed", - }, { name: "no git spec", obj: &provisioning.Repository{ @@ -106,11 +31,7 @@ func TestMutator(t *testing.T) { Git: nil, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, }, - { name: "no git spec for git repository type", obj: &provisioning.Repository{ @@ -123,9 +44,6 @@ func TestMutator(t *testing.T) { Git: nil, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedError: "git configuration is required for git repository type", }, { @@ -137,21 +55,13 @@ func TestMutator(t *testing.T) { }, Spec: provisioning.RepositorySpec{ Type: provisioning.GitRepositoryType, - Git: &provisioning.GitRepositoryConfig{ - Token: "", - }, + Git: &provisioning.GitRepositoryConfig{}, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, }, { name: "non-repository object", obj: &runtime.Unknown{}, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, }, { name: "URL normalization - add .git suffix", @@ -167,9 +77,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedURL: "https://github.com/grafana/grafana.git", }, { @@ -186,9 +93,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedURL: "https://github.com/grafana/grafana.git", }, { @@ -205,9 +109,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedURL: "https://github.com/grafana/grafana.git", }, { @@ -224,9 +125,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedURL: "https://github.com/grafana/grafana.git", }, { @@ -243,19 +141,13 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - // No expectations - }, expectedURL: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMocks(mockSecrets) - - mutator := Mutator(mockSecrets) + mutator := Mutator() err := mutator(context.Background(), tt.obj) if tt.expectedError != "" { @@ -266,13 +158,6 @@ func TestMutator(t *testing.T) { // Check that token was cleared and encrypted token was set if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.Git != nil { - if tt.expectedEncryptedToken != "" { - // Token should be cleared after encryption - assert.Empty(t, repo.Spec.Git.Token, "Token should be cleared after encryption") - // EncryptedToken should be set to the expected value - assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.Git.EncryptedToken), "EncryptedToken should match expected value") - } - // Check URL normalization if tt.expectedURL != "" { assert.Equal(t, tt.expectedURL, repo.Spec.Git.URL, "URL should be normalized correctly") diff --git a/pkg/registry/apis/provisioning/repository/git/repository.go b/pkg/registry/apis/provisioning/repository/git/repository.go index 41c3a328788..04d2d53568a 100644 --- a/pkg/registry/apis/provisioning/repository/git/repository.go +++ b/pkg/registry/apis/provisioning/repository/git/repository.go @@ -17,9 +17,9 @@ import ( "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/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" "github.com/grafana/nanogit" "github.com/grafana/nanogit/log" "github.com/grafana/nanogit/options" @@ -27,16 +27,12 @@ import ( "github.com/grafana/nanogit/protocol/hash" ) -//nolint:gosec // This is a constant for a secret suffix -const gitTokenSecretSuffix = "-git-token" - type RepositoryConfig struct { - URL string - Branch string - TokenUser string - Token string - EncryptedToken []byte - Path string + URL string + Branch string + TokenUser string + Token common.RawSecureValue + Path string } // Make sure all public functions of this struct call the (*gitRepository).logger function, to ensure the Git repo details are included. @@ -44,23 +40,21 @@ type gitRepository struct { config *provisioning.Repository gitConfig RepositoryConfig client nanogit.Client - secrets secrets.RepositorySecrets } func NewGitRepository( ctx context.Context, config *provisioning.Repository, gitConfig RepositoryConfig, - secrets secrets.RepositorySecrets, ) (GitRepository, error) { var opts []options.Option - if len(gitConfig.Token) > 0 { + if !gitConfig.Token.IsZero() { tokenUser := gitConfig.TokenUser if tokenUser == "" { tokenUser = "git" } - opts = append(opts, options.WithBasicAuth(tokenUser, gitConfig.Token)) + opts = append(opts, options.WithBasicAuth(tokenUser, string(gitConfig.Token))) } client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...) @@ -72,7 +66,6 @@ func NewGitRepository( config: config, gitConfig: gitConfig, client: client, - secrets: secrets, }, nil } @@ -106,10 +99,10 @@ func (r *gitRepository) Validate() (list field.ErrorList) { list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name")) } - // If the repository has workflows, we require a token or encrypted token + // Readonly repositories may not need a token (if public) if len(r.config.Spec.Workflows) > 0 { - if cfg.Token == "" && len(cfg.EncryptedToken) == 0 { - list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required")) + if cfg.Token == "" && r.config.Secure.Token.IsZero() { + list = append(list, field.Required(field.NewPath("secure", "token"), "a git access token is required")) } } @@ -166,7 +159,7 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er Success: false, Errors: []provisioning.ErrorDetails{{ Type: metav1.CauseTypeFieldValueInvalid, - Field: field.NewPath("spec", t, "token").String(), + Field: field.NewPath("secure", "token").String(), Detail: detail, }}, }, nil @@ -832,23 +825,3 @@ func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context return ctx, logger } - -func (r *gitRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) { - return nil, nil -} - -func (r *gitRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) { - return nil, nil -} - -func (r *gitRepository) OnDelete(ctx context.Context) error { - logger := logging.FromContext(ctx) - secretName := r.config.Name + gitTokenSecretSuffix - if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { - return fmt.Errorf("delete git token secret: %w", err) - } - - logger.Info("Deleted git token secret", "secretName", secretName) - - return nil -} diff --git a/pkg/registry/apis/provisioning/repository/git/repository_test.go b/pkg/registry/apis/provisioning/repository/git/repository_test.go index 4236352f94c..2b556110beb 100644 --- a/pkg/registry/apis/provisioning/repository/git/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/git/repository_test.go @@ -14,7 +14,6 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/nanogit" "github.com/grafana/nanogit/mocks" "github.com/grafana/nanogit/protocol" @@ -152,7 +151,7 @@ func TestGitRepository_Validate(t *testing.T) { Token: "", // Empty token }, want: field.ErrorList{ - field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"), + field.Required(field.NewPath("secure", "token"), "a git access token is required"), }, }, { @@ -291,10 +290,9 @@ func TestNewGit(t *testing.T) { Path: "configs", } - mockSecrets := secrets.NewMockRepositorySecrets(t) // This should succeed in creating the client but won't be able to connect // We just test that the basic structure is created correctly - gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets) + gitRepo, err := NewGitRepository(ctx, config, gitConfig) require.NoError(t, err) require.NotNil(t, gitRepo) require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL()) @@ -502,7 +500,7 @@ func TestGitRepository_Test(t *testing.T) { Errors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, - Field: field.NewPath("spec", "test_type", "token").String(), + Field: field.NewPath("secure", "token").String(), Detail: "failed check if authorized: auth error", }, }, @@ -523,7 +521,7 @@ func TestGitRepository_Test(t *testing.T) { Errors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, - Field: field.NewPath("spec", "test_type", "token").String(), + Field: field.NewPath("secure", "token").String(), Detail: "not authorized", }, }, @@ -1862,8 +1860,7 @@ func TestNewGitRepository(t *testing.T) { }, } - mockSecrets := secrets.NewMockRepositorySecrets(t) - gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig, mockSecrets) + gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig) if tt.wantError { require.Error(t, err) @@ -2822,8 +2819,7 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) { Path: "configs", } - mockSecrets := secrets.NewMockRepositorySecrets(t) - gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets) + gitRepo, err := NewGitRepository(ctx, config, gitConfig) // We expect this to fail during client creation require.Error(t, err) @@ -3737,82 +3733,6 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te } } -func TestGitRepository_OnDelete(t *testing.T) { - tests := []struct { - name string - setupMock func(*secrets.MockRepositorySecrets) - config *provisioning.Repository - expectedError string - }{ - { - name: "successful secret deletion", - setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Delete( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - "test-repo"+gitTokenSecretSuffix, - ).Return(nil) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - }, - { - name: "secret deletion error", - setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Delete( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - "test-repo"+gitTokenSecretSuffix, - ).Return(errors.New("failed to delete secret")) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - expectedError: "delete git token secret: failed to delete secret", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMock(mockSecrets) - - gitRepo := &gitRepository{ - config: tt.config, - secrets: mockSecrets, - } - - err := gitRepo.OnDelete(context.Background()) - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - } else { - require.NoError(t, err) - } - - mockSecrets.AssertExpectations(t) - }) - } -} - func TestGitRepository_Move(t *testing.T) { tests := []struct { name string diff --git a/pkg/registry/apis/provisioning/repository/github/factory.go b/pkg/registry/apis/provisioning/repository/github/factory.go index 6f2435ee8d0..f11a7af4c85 100644 --- a/pkg/registry/apis/provisioning/repository/github/factory.go +++ b/pkg/registry/apis/provisioning/repository/github/factory.go @@ -6,6 +6,8 @@ import ( "github.com/google/go-github/v70/github" "golang.org/x/oauth2" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // Factory creates new GitHub clients. @@ -20,16 +22,15 @@ func ProvideFactory() *Factory { return &Factory{} } -func (r *Factory) New(ctx context.Context, ghToken string) Client { +func (r *Factory) New(ctx context.Context, ghToken common.RawSecureValue) Client { if r.Client != nil { return NewClient(github.NewClient(r.Client)) } - tokenSrc := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: ghToken}, - ) - - if len(ghToken) > 0 { + if !ghToken.IsZero() { + tokenSrc := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: string(ghToken)}, + ) tokenClient := oauth2.NewClient(ctx, tokenSrc) return NewClient(github.NewClient(tokenClient)) } diff --git a/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go index e5fc32897cf..a5245efd4a2 100644 --- a/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package github @@ -503,168 +503,6 @@ func (_c *MockGithubRepository_Move_Call) RunAndReturn(run func(context.Context, return _c } -// OnCreate provides a mock function with given fields: ctx -func (_m *MockGithubRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for OnCreate") - } - - var r0 []map[string]interface{} - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]map[string]interface{}) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGithubRepository_OnCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreate' -type MockGithubRepository_OnCreate_Call struct { - *mock.Call -} - -// OnCreate is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockGithubRepository_Expecter) OnCreate(ctx interface{}) *MockGithubRepository_OnCreate_Call { - return &MockGithubRepository_OnCreate_Call{Call: _e.mock.On("OnCreate", ctx)} -} - -func (_c *MockGithubRepository_OnCreate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnCreate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockGithubRepository_OnCreate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnCreate_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGithubRepository_OnCreate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnCreate_Call { - _c.Call.Return(run) - return _c -} - -// OnDelete provides a mock function with given fields: ctx -func (_m *MockGithubRepository) OnDelete(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for OnDelete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockGithubRepository_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete' -type MockGithubRepository_OnDelete_Call struct { - *mock.Call -} - -// OnDelete is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockGithubRepository_Expecter) OnDelete(ctx interface{}) *MockGithubRepository_OnDelete_Call { - return &MockGithubRepository_OnDelete_Call{Call: _e.mock.On("OnDelete", ctx)} -} - -func (_c *MockGithubRepository_OnDelete_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnDelete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockGithubRepository_OnDelete_Call) Return(_a0 error) *MockGithubRepository_OnDelete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockGithubRepository_OnDelete_Call) RunAndReturn(run func(context.Context) error) *MockGithubRepository_OnDelete_Call { - _c.Call.Return(run) - return _c -} - -// OnUpdate provides a mock function with given fields: ctx -func (_m *MockGithubRepository) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for OnUpdate") - } - - var r0 []map[string]interface{} - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]map[string]interface{}) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGithubRepository_OnUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdate' -type MockGithubRepository_OnUpdate_Call struct { - *mock.Call -} - -// OnUpdate is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockGithubRepository_Expecter) OnUpdate(ctx interface{}) *MockGithubRepository_OnUpdate_Call { - return &MockGithubRepository_OnUpdate_Call{Call: _e.mock.On("OnUpdate", ctx)} -} - -func (_c *MockGithubRepository_OnUpdate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnUpdate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockGithubRepository_OnUpdate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnUpdate_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGithubRepository_OnUpdate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnUpdate_Call { - _c.Call.Return(run) - return _c -} - // Owner provides a mock function with no fields func (_m *MockGithubRepository) Owner() string { ret := _m.Called() diff --git a/pkg/registry/apis/provisioning/repository/github/mock_client.go b/pkg/registry/apis/provisioning/repository/github/mock_client.go index f70aa9f1359..0cbc41d1356 100644 --- a/pkg/registry/apis/provisioning/repository/github/mock_client.go +++ b/pkg/registry/apis/provisioning/repository/github/mock_client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package github diff --git a/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go b/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go index 68d3d2044f0..29e91059f90 100644 --- a/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go +++ b/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package github diff --git a/pkg/registry/apis/provisioning/repository/github/mutator.go b/pkg/registry/apis/provisioning/repository/github/mutator.go index b0ee0e54a24..1a0b165cf9b 100644 --- a/pkg/registry/apis/provisioning/repository/github/mutator.go +++ b/pkg/registry/apis/provisioning/repository/github/mutator.go @@ -8,10 +8,9 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" ) -func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { +func Mutator() controller.Mutator { return func(ctx context.Context, obj runtime.Object) error { repo, ok := obj.(*provisioning.Repository) if !ok { @@ -31,16 +30,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { repo.Spec.GitHub.URL = url } - if repo.Spec.GitHub.Token != "" { - secretName := repo.Name + githubTokenSecretSuffix - nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.GitHub.Token) - if err != nil { - return err - } - repo.Spec.GitHub.EncryptedToken = nameOrValue - repo.Spec.GitHub.Token = "" - } - return nil } } diff --git a/pkg/registry/apis/provisioning/repository/github/mutator_test.go b/pkg/registry/apis/provisioning/repository/github/mutator_test.go index 5094ed07206..dfeffa485ea 100644 --- a/pkg/registry/apis/provisioning/repository/github/mutator_test.go +++ b/pkg/registry/apis/provisioning/repository/github/mutator_test.go @@ -2,25 +2,21 @@ package github import ( "context" - "errors" "testing" - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" ) func TestMutator(t *testing.T) { tests := []struct { - name string - obj runtime.Object - token string - setupMocks func(*secrets.MockRepositorySecrets) - expectedToken string - expectedEncryptedToken string - expectedError string + name string + obj runtime.Object + token string + expectedError string }{ { name: "trims trailing .git and slash from GitHub URL", @@ -35,9 +31,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {}, - expectedToken: "", - expectedEncryptedToken: "", }, { name: "trims only trailing slash from GitHub URL", @@ -52,9 +45,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {}, - expectedToken: "", - expectedEncryptedToken: "", }, { name: "trims only trailing .git from GitHub URL", @@ -69,9 +59,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {}, - expectedToken: "", - expectedEncryptedToken: "", }, { name: "does not trim if no .git or slash", @@ -86,76 +73,6 @@ func TestMutator(t *testing.T) { }, }, }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {}, - expectedToken: "", - expectedEncryptedToken: "", - }, - { - name: "successful token encryption", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - "test-repo"+githubTokenSecretSuffix, - "secret-token", - ).Return([]byte("encrypted-token"), nil) - }, - expectedToken: "", - expectedEncryptedToken: "encrypted-token", - }, - { - name: "encryption error", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Token: "secret-token", - }, - }, - }, - "test-repo"+githubTokenSecretSuffix, - "secret-token", - ).Return(nil, errors.New("encryption failed")) - }, - expectedError: "encryption failed", }, { name: "no github spec", @@ -168,9 +85,6 @@ func TestMutator(t *testing.T) { GitHub: nil, }, }, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, }, { name: "empty token", @@ -180,30 +94,19 @@ func TestMutator(t *testing.T) { Namespace: "default", }, Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Token: "", - }, + GitHub: &provisioning.GitHubRepositoryConfig{}, }, }, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, }, { name: "non-repository object", obj: &runtime.Unknown{}, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMocks(mockSecrets) - - mutator := Mutator(mockSecrets) + mutator := Mutator() err := mutator(context.Background(), tt.obj) if tt.expectedError != "" { @@ -211,16 +114,6 @@ func TestMutator(t *testing.T) { assert.Contains(t, err.Error(), tt.expectedError) } else { assert.NoError(t, err) - - // Check that token was cleared and encrypted token was set - if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.GitHub != nil { - if tt.expectedEncryptedToken != "" { - // Token should be cleared after encryption - assert.Empty(t, repo.Spec.GitHub.Token, "Token should be cleared after encryption") - // EncryptedToken should be set to the expected value - assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.GitHub.EncryptedToken), "EncryptedToken should match expected value") - } - } } }) } diff --git a/pkg/registry/apis/provisioning/repository/github/repository.go b/pkg/registry/apis/provisioning/repository/github/repository.go index 00ff70eb213..8985e8d8f59 100644 --- a/pkg/registry/apis/provisioning/repository/github/repository.go +++ b/pkg/registry/apis/provisioning/repository/github/repository.go @@ -7,25 +7,20 @@ import ( "net/url" "strings" - "github.com/grafana/grafana-app-sdk/logging" "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" ) -//nolint:gosec // This is a constant for a secret suffix -const githubTokenSecretSuffix = "-github-token" - // Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. type githubRepository struct { git.GitRepository - config *provisioning.Repository - gh Client // assumes github.com base URL - secrets secrets.RepositorySecrets + config *provisioning.Repository + gh Client // assumes github.com base URL owner string repo string @@ -42,7 +37,6 @@ type GithubRepository interface { repository.Reader repository.RepositoryWithURLs repository.StageableRepository - repository.Hooks Owner() string Repo() string Client() Client @@ -53,8 +47,7 @@ func NewGitHub( config *provisioning.Repository, gitRepo git.GitRepository, factory *Factory, - token string, - secrets secrets.RepositorySecrets, + token common.RawSecureValue, ) (GithubRepository, error) { owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL) if err != nil { @@ -67,7 +60,6 @@ func NewGitHub( gh: factory.New(ctx, token), // TODO, baseURL from config owner: owner, repo: repo, - secrets: secrets, }, nil } @@ -242,23 +234,3 @@ func (r *githubRepository) RefURLs(ctx context.Context, ref string) (*provisioni return urls, nil } - -func (r *githubRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) { - return nil, nil -} - -func (r *githubRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) { - return nil, nil -} - -func (r *githubRepository) OnDelete(ctx context.Context) error { - logger := logging.FromContext(ctx) - secretName := r.config.Name + githubTokenSecretSuffix - if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { - return fmt.Errorf("delete github token secret: %w", err) - } - - logger.Info("Deleted github token secret", "secretName", secretName) - - return nil -} diff --git a/pkg/registry/apis/provisioning/repository/github/repository_test.go b/pkg/registry/apis/provisioning/repository/github/repository_test.go index 94d9c938cd5..1e91242ca61 100644 --- a/pkg/registry/apis/provisioning/repository/github/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/github/repository_test.go @@ -15,9 +15,9 @@ import ( field "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" ) func TestNewGitHub(t *testing.T) { @@ -81,16 +81,13 @@ func TestNewGitHub(t *testing.T) { gitRepo := git.NewMockGitRepository(t) - mockSecrets := secrets.NewMockRepositorySecrets(t) - // Call the function under test repo, err := NewGitHub( context.Background(), tt.config, gitRepo, factory, - tt.token, - mockSecrets, + common.RawSecureValue(tt.token), ) // Check results @@ -179,10 +176,14 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", - Token: "valid-token", Path: "dashboards", }, }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", + }, + }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ @@ -190,10 +191,14 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", - Token: "valid-token", Path: "dashboards", }, }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", + }, + }, }) m.On("Validate").Return(field.ErrorList{}) }, @@ -223,7 +228,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }, @@ -233,7 +242,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }) @@ -248,7 +261,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }, @@ -258,7 +275,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }) @@ -273,7 +294,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://gitlab.com/grafana/grafana", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }, @@ -283,7 +308,11 @@ func TestGitHubRepositoryValidate(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://gitlab.com/grafana/grafana", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }) @@ -339,7 +368,11 @@ func TestGitHubRepositoryTest(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }, @@ -361,7 +394,11 @@ func TestGitHubRepositoryTest(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", - Token: "valid-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, }, @@ -878,7 +915,11 @@ func TestGitHubRepositoryDelegation(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", - Token: "test-token", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Name: "with-name", }, }, } @@ -1095,7 +1136,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) { GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", - Token: "test-token", }, }, } @@ -1137,82 +1177,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) { }) } -func TestGitHubRepository_OnDelete(t *testing.T) { - tests := []struct { - name string - setupMock func(*secrets.MockRepositorySecrets) - config *provisioning.Repository - expectedError string - }{ - { - name: "successful secret deletion", - setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Delete( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - "test-repo"+githubTokenSecretSuffix, - ).Return(nil) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - }, - { - name: "secret deletion error", - setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Delete( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - "test-repo"+githubTokenSecretSuffix, - ).Return(errors.New("failed to delete secret")) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - }, - expectedError: "delete github token secret: failed to delete secret", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMock(mockSecrets) - - githubRepo := &githubRepository{ - config: tt.config, - secrets: mockSecrets, - } - - err := githubRepo.OnDelete(context.Background()) - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - } else { - require.NoError(t, err) - } - - mockSecrets.AssertExpectations(t) - }) - } -} - func TestGithubRepository_Move(t *testing.T) { tests := []struct { name string @@ -1262,7 +1226,6 @@ func TestGithubRepository_Move(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create mock git repository mockGitRepo := git.NewMockGitRepository(t) - mockSecrets := &secrets.MockRepositorySecrets{} // Setup mock expectations tt.setupMock(mockGitRepo) @@ -1285,7 +1248,6 @@ func TestGithubRepository_Move(t *testing.T) { GitRepository: mockGitRepo, owner: "example", repo: "repo", - secrets: mockSecrets, } // Execute move operation diff --git a/pkg/registry/apis/provisioning/repository/secure.go b/pkg/registry/apis/provisioning/repository/secure.go new file mode 100644 index 00000000000..bce75301875 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/secure.go @@ -0,0 +1,59 @@ +package repository + +import ( + "context" + "fmt" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type Decrypter = func(r *provisioning.Repository) SecureValues + +type SecureValues interface { + Token(ctx context.Context) (common.RawSecureValue, error) + WebhookSecret(ctx context.Context) (common.RawSecureValue, error) +} + +type secureValues struct { + svc contracts.DecryptService + names provisioning.SecureValues + namespace string +} + +func (s *secureValues) get(ctx context.Context, sv common.InlineSecureValue) (common.RawSecureValue, error) { + if !sv.Create.IsZero() { + return sv.Create, nil // If this was called before the value is actually saved + } + if sv.Name == "" { + return "", nil + } + results, err := s.svc.Decrypt(ctx, provisioning.GROUP, s.namespace, sv.Name) + if err != nil { + return "", fmt.Errorf("failed to call decrypt service: %w", err) + } + + v, found := results[sv.Name] + if !found { + return "", fmt.Errorf("not found") + } + if v.Error() != nil { + return "", v.Error() + } + return common.RawSecureValue(*v.Value()), nil +} + +func (s *secureValues) Token(ctx context.Context) (common.RawSecureValue, error) { + return s.get(ctx, s.names.Token) +} + +func (s *secureValues) WebhookSecret(ctx context.Context) (common.RawSecureValue, error) { + return s.get(ctx, s.names.WebhookSecret) +} + +func DecryptService(svc contracts.DecryptService) Decrypter { + return func(r *provisioning.Repository) SecureValues { + return &secureValues{svc: svc, names: r.Secure, namespace: r.Namespace} + } +} diff --git a/pkg/registry/apis/provisioning/repository/secure_test.go b/pkg/registry/apis/provisioning/repository/secure_test.go new file mode 100644 index 00000000000..b93aff27164 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/secure_test.go @@ -0,0 +1,161 @@ +package repository + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +func TestRepositorySecureValues(t *testing.T) { + type expectedDecryptedResult struct { + value string + error string + } + + tests := []struct { + name string + config *provisioning.Repository + decrypt decryptFn + token expectedDecryptedResult + webhook expectedDecryptedResult + }{ + { + name: "referenced by name", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{ + Token: v0alpha1.InlineSecureValue{ + Name: "secret", + }, + }, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + require.Equal(t, []string{"secret"}, names) + val := secretv1beta1.NewExposedSecureValue(names[0]) + return map[string]contracts.DecryptResult{ + names[0]: contracts.NewDecryptResultValue(&val), + }, nil + }, + token: expectedDecryptedResult{ + value: "secret", + }, + }, + { + name: "when create exists, use it", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{ + Token: v0alpha1.InlineSecureValue{ + Create: "secret", + }, + }, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + t.Fatal("decrypt should not be called when Create is set") + return nil, nil + }, + token: expectedDecryptedResult{ + value: "secret", + }, + }, + { + name: "avoid decrypt when no values are configured", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{}, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + t.Fatal("decrypt should not be called when no values are configured") + return nil, nil + }, + }, + { + name: "propagate error from service", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{ + WebhookSecret: v0alpha1.InlineSecureValue{ + Name: "secret", + }, + }, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + require.Equal(t, []string{"secret"}, names) + return map[string]contracts.DecryptResult{ + names[0]: contracts.NewDecryptResultErr(fmt.Errorf("error for name")), + }, nil + }, + webhook: expectedDecryptedResult{ + error: "error for name", + }, + }, + { + name: "not found", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{ + Token: v0alpha1.InlineSecureValue{ + Name: "secret", + }, + }, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + return map[string]contracts.DecryptResult{}, nil + }, + token: expectedDecryptedResult{ + error: "not found", // it is not in the results above + }, + }, + { + name: "not found", + config: &provisioning.Repository{ + Secure: provisioning.SecureValues{ + WebhookSecret: v0alpha1.InlineSecureValue{ + Name: "secret", + }, + }, + }, + decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) { + return nil, fmt.Errorf("explode") + }, + webhook: expectedDecryptedResult{ + error: "failed to call decrypt service", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decrypter := DecryptService(&dummyDecryptService{t: t, fn: tt.decrypt}) + decrypted := decrypter(tt.config) + + token, err := decrypted.Token(context.Background()) + if tt.token.error != "" { + require.ErrorContains(t, err, tt.token.error) + } else { + require.NoError(t, err) + require.Equal(t, tt.token.value, string(token)) + } + + webhook, err := decrypted.WebhookSecret(context.Background()) + if tt.webhook.error != "" { + require.ErrorContains(t, err, tt.webhook.error) + } else { + require.NoError(t, err) + require.Equal(t, tt.webhook.value, string(webhook)) + } + }) + } +} + +type decryptFn = func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) + +type dummyDecryptService struct { + t *testing.T + fn decryptFn +} + +func (d *dummyDecryptService) Decrypt(_ context.Context, _ string, _ string, names ...string) (map[string]contracts.DecryptResult, error) { + return d.fn(d.t, names...) +} diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret.go b/pkg/registry/apis/provisioning/secrets/legacy_secret.go deleted file mode 100644 index 6190e1389ff..00000000000 --- a/pkg/registry/apis/provisioning/secrets/legacy_secret.go +++ /dev/null @@ -1,37 +0,0 @@ -package secrets - -import ( - "context" - - "github.com/grafana/grafana/pkg/services/secrets" -) - -// A secrets encryption service. It only operates on values, no names or similar. -// It is likely we will need to change this when the multi-tenant service comes around. -// -// FIXME: this is a temporary service/package until we can make use of -// the new secrets service in app platform. -// -//go:generate mockery --name LegacyService --structname MockLegacyService --inpackage --filename legacy_secret_mock.go --with-expecter -type LegacyService interface { - Encrypt(ctx context.Context, data []byte) ([]byte, error) - Decrypt(ctx context.Context, data []byte) ([]byte, error) -} - -var _ LegacyService = (*singleTenant)(nil) - -type singleTenant struct { - inner secrets.Service -} - -func NewSingleTenant(svc secrets.Service) LegacyService { - return &singleTenant{svc} -} - -func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Encrypt(ctx, data, secrets.WithoutScope()) -} - -func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Decrypt(ctx, data) -} diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go b/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go deleted file mode 100644 index 01f3ee1f853..00000000000 --- a/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go +++ /dev/null @@ -1,154 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package secrets - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockLegacyService is an autogenerated mock type for the LegacyService type -type MockLegacyService struct { - mock.Mock -} - -type MockLegacyService_Expecter struct { - mock *mock.Mock -} - -func (_m *MockLegacyService) EXPECT() *MockLegacyService_Expecter { - return &MockLegacyService_Expecter{mock: &_m.Mock} -} - -// Decrypt provides a mock function with given fields: ctx, data -func (_m *MockLegacyService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) - - if len(ret) == 0 { - panic("no return value specified for Decrypt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) - } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockLegacyService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' -type MockLegacyService_Decrypt_Call struct { - *mock.Call -} - -// Decrypt is a helper method to define mock.On call -// - ctx context.Context -// - data []byte -func (_e *MockLegacyService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockLegacyService_Decrypt_Call { - return &MockLegacyService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)} -} - -func (_c *MockLegacyService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Decrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) - }) - return _c -} - -func (_c *MockLegacyService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Decrypt_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockLegacyService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Decrypt_Call { - _c.Call.Return(run) - return _c -} - -// Encrypt provides a mock function with given fields: ctx, data -func (_m *MockLegacyService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) - - if len(ret) == 0 { - panic("no return value specified for Encrypt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) - } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockLegacyService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' -type MockLegacyService_Encrypt_Call struct { - *mock.Call -} - -// Encrypt is a helper method to define mock.On call -// - ctx context.Context -// - data []byte -func (_e *MockLegacyService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockLegacyService_Encrypt_Call { - return &MockLegacyService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)} -} - -func (_c *MockLegacyService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Encrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) - }) - return _c -} - -func (_c *MockLegacyService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Encrypt_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockLegacyService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Encrypt_Call { - _c.Call.Return(run) - return _c -} - -// NewMockLegacyService creates a new instance of MockLegacyService. 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 NewMockLegacyService(t interface { - mock.TestingT - Cleanup(func()) -}) *MockLegacyService { - mock := &MockLegacyService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/secrets/repository.go b/pkg/registry/apis/provisioning/secrets/repository.go deleted file mode 100644 index c6f90cc79b3..00000000000 --- a/pkg/registry/apis/provisioning/secrets/repository.go +++ /dev/null @@ -1,112 +0,0 @@ -package secrets - -import ( - "context" - "errors" - "strings" - - "github.com/grafana/grafana-app-sdk/logging" - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/featuremgmt" - legacysecrets "github.com/grafana/grafana/pkg/services/secrets" - "github.com/grafana/grafana/pkg/setting" -) - -func ProvideRepositorySecrets( - features featuremgmt.FeatureToggles, - legacySecretsSvc legacysecrets.Service, - secretsSvc contracts.SecureValueClient, - decryptSvc secret.DecryptService, - cfg *setting.Cfg, -) RepositorySecrets { - return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc, cfg.SecretsManagement.GrpcGrafanaServiceName), NewSingleTenant(legacySecretsSvc)) -} - -//go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter -type RepositorySecrets interface { - Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) - Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) (data []byte, err error) - Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error -} - -// repositorySecrets provides a unified interface for encrypting and decrypting repository secrets, -// supporting both the legacy and new secrets services. The active backend is determined by the -// FlagProvisioningSecretsService feature flag: -// - If enabled, operations use the new secrets service. -// - If disabled, operations use the legacy secrets service. -// -// This abstraction enables a seamless migration path between secret backends without breaking -// existing functionality. Once migration is complete and the legacy service is deprecated, -// this wrapper should be removed. -type repositorySecrets struct { - features featuremgmt.FeatureToggles - secretsSvc Service - legacySecrets LegacyService -} - -func NewRepositorySecrets(features featuremgmt.FeatureToggles, secretsSvc Service, legacySecrets LegacyService) RepositorySecrets { - return &repositorySecrets{ - features: features, - secretsSvc: secretsSvc, - legacySecrets: legacySecrets, - } -} - -// Encrypt encrypts the data and returns the name or value of the encrypted data -// If the feature flag is disabled, it uses the legacy secrets service -// If the feature flag is enabled, it uses the secrets service -func (s *repositorySecrets) Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) { - logger := logging.FromContext(ctx).With("name", name, "namespace", r.GetNamespace()) - if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { - logger.Info("Encrypting secret with new secrets service") - encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data) - if err != nil { - return nil, err - } - return []byte(encrypted), err - } - - logger.Info("Encrypting secret with legacy secrets service") - encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data)) - if err != nil { - return nil, err - } - - return encrypted, nil -} - -// Decrypt retrieves and decrypts secret data for a repository, supporting migration between secret backends. -// The backend used for decryption is determined by a heuristic: -// - If the provided nameOrValue starts with the repository name, it is assumed to be a Kubernetes secret name -// and the new secrets service is used for decryption. -// - Otherwise, it is treated as a legacy secret value and the legacy secrets service is used. -// -// HACK: This approach relies on checking the prefix of nameOrValue to distinguish between secret backends. -// This is a temporary workaround to support both backends during migration and should be removed once -// migration is complete. -// -// This method ensures compatibility and minimizes disruption during the transition between secret backends. -func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) { - logger := logging.FromContext(ctx) - // HACK: this is a hack to identify if the name is a potential Kubernetes name for a secret. - if strings.HasPrefix(nameOrValue, r.GetName()) { - logger.Info("Decrypting secret with new secrets service") - return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) - } else { - logger.Info("Decrypting secret with legacy secrets service") - return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) - } -} - -func (s *repositorySecrets) Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error { - if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { - err := s.secretsSvc.Delete(ctx, r.GetNamespace(), nameOrValue) - if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) { - return err - } - } - - return nil -} diff --git a/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go b/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go deleted file mode 100644 index 7108be570f2..00000000000 --- a/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go +++ /dev/null @@ -1,206 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package secrets - -import ( - context "context" - - v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - mock "github.com/stretchr/testify/mock" -) - -// MockRepositorySecrets is an autogenerated mock type for the RepositorySecrets type -type MockRepositorySecrets struct { - mock.Mock -} - -type MockRepositorySecrets_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRepositorySecrets) EXPECT() *MockRepositorySecrets_Expecter { - return &MockRepositorySecrets_Expecter{mock: &_m.Mock} -} - -// Decrypt provides a mock function with given fields: ctx, r, nameOrValue -func (_m *MockRepositorySecrets) Decrypt(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) ([]byte, error) { - ret := _m.Called(ctx, r, nameOrValue) - - if len(ret) == 0 { - panic("no return value specified for Decrypt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) ([]byte, error)); ok { - return rf(ctx, r, nameOrValue) - } - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) []byte); ok { - r0 = rf(ctx, r, nameOrValue) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string) error); ok { - r1 = rf(ctx, r, nameOrValue) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositorySecrets_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' -type MockRepositorySecrets_Decrypt_Call struct { - *mock.Call -} - -// Decrypt is a helper method to define mock.On call -// - ctx context.Context -// - r *v0alpha1.Repository -// - nameOrValue string -func (_e *MockRepositorySecrets_Expecter) Decrypt(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Decrypt_Call { - return &MockRepositorySecrets_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, r, nameOrValue)} -} - -func (_c *MockRepositorySecrets_Decrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Decrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string)) - }) - return _c -} - -func (_c *MockRepositorySecrets_Decrypt_Call) Return(data []byte, err error) *MockRepositorySecrets_Decrypt_Call { - _c.Call.Return(data, err) - return _c -} - -func (_c *MockRepositorySecrets_Decrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) ([]byte, error)) *MockRepositorySecrets_Decrypt_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, r, nameOrValue -func (_m *MockRepositorySecrets) Delete(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) error { - ret := _m.Called(ctx, r, nameOrValue) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) error); ok { - r0 = rf(ctx, r, nameOrValue) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockRepositorySecrets_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockRepositorySecrets_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - r *v0alpha1.Repository -// - nameOrValue string -func (_e *MockRepositorySecrets_Expecter) Delete(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Delete_Call { - return &MockRepositorySecrets_Delete_Call{Call: _e.mock.On("Delete", ctx, r, nameOrValue)} -} - -func (_c *MockRepositorySecrets_Delete_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string)) - }) - return _c -} - -func (_c *MockRepositorySecrets_Delete_Call) Return(_a0 error) *MockRepositorySecrets_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositorySecrets_Delete_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) error) *MockRepositorySecrets_Delete_Call { - _c.Call.Return(run) - return _c -} - -// Encrypt provides a mock function with given fields: ctx, r, name, data -func (_m *MockRepositorySecrets) Encrypt(ctx context.Context, r *v0alpha1.Repository, name string, data string) ([]byte, error) { - ret := _m.Called(ctx, r, name, data) - - if len(ret) == 0 { - panic("no return value specified for Encrypt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)); ok { - return rf(ctx, r, name, data) - } - if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) []byte); ok { - r0 = rf(ctx, r, name, data) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string, string) error); ok { - r1 = rf(ctx, r, name, data) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositorySecrets_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' -type MockRepositorySecrets_Encrypt_Call struct { - *mock.Call -} - -// Encrypt is a helper method to define mock.On call -// - ctx context.Context -// - r *v0alpha1.Repository -// - name string -// - data string -func (_e *MockRepositorySecrets_Expecter) Encrypt(ctx interface{}, r interface{}, name interface{}, data interface{}) *MockRepositorySecrets_Encrypt_Call { - return &MockRepositorySecrets_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, r, name, data)} -} - -func (_c *MockRepositorySecrets_Encrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, name string, data string)) *MockRepositorySecrets_Encrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockRepositorySecrets_Encrypt_Call) Return(nameOrValue []byte, err error) *MockRepositorySecrets_Encrypt_Call { - _c.Call.Return(nameOrValue, err) - return _c -} - -func (_c *MockRepositorySecrets_Encrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)) *MockRepositorySecrets_Encrypt_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRepositorySecrets creates a new instance of MockRepositorySecrets. 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 NewMockRepositorySecrets(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRepositorySecrets { - mock := &MockRepositorySecrets{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/secrets/repository_test.go b/pkg/registry/apis/provisioning/secrets/repository_test.go deleted file mode 100644 index bd53089fc75..00000000000 --- a/pkg/registry/apis/provisioning/secrets/repository_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package secrets - -import ( - "context" - "errors" - "testing" - - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type testSetup struct { - rs RepositorySecrets - mockFeatures *featuremgmt.MockFeatureToggles - mockSecrets *MockService - mockLegacy *MockLegacyService - repo *provisioning.Repository - ctx context.Context -} - -func setupTest(t *testing.T, namespace string) *testSetup { - mockFeatures := featuremgmt.NewMockFeatureToggles(t) - mockSecrets := NewMockService(t) - mockLegacy := NewMockLegacyService(t) - - rs := NewRepositorySecrets(mockFeatures, mockSecrets, mockLegacy) - - repo := &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: namespace, - }, - } - - return &testSetup{ - rs: rs, - mockFeatures: mockFeatures, - mockSecrets: mockSecrets, - mockLegacy: mockLegacy, - repo: repo, - ctx: context.Background(), - } -} - -func (s *testSetup) expectFeatureFlag(enabled bool) { - s.mockFeatures.EXPECT().IsEnabled( - mock.AnythingOfType("context.backgroundCtx"), - featuremgmt.FlagProvisioningSecretsService, - ).Return(enabled) -} - -func TestRepositorySecrets_Encrypt(t *testing.T) { - tests := []struct { - name string - namespace string - featureEnabled bool - setupMocks func(*testSetup) - expectedResult []byte - expectedError string - }{ - { - name: "new service success", - namespace: "test-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("encrypted-name", nil) - }, - expectedResult: []byte("encrypted-name"), - }, - { - name: "legacy service success", - namespace: "test-namespace", - featureEnabled: false, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(false) - s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return([]byte("encrypted-legacy-data"), nil) - }, - expectedResult: []byte("encrypted-legacy-data"), - }, - { - name: "new service error", - namespace: "test-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("", errors.New("encryption failed")) - }, - expectedError: "encryption failed", - }, - { - name: "legacy service error", - namespace: "test-namespace", - featureEnabled: false, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(false) - s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return(nil, errors.New("legacy encryption failed")) - }, - expectedError: "legacy encryption failed", - }, - { - name: "empty namespace handling", - namespace: "", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Encrypt(s.ctx, "", "test-secret", "secret-data").Return("encrypted", nil) - }, - expectedResult: []byte("encrypted"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setup := setupTest(t, tt.namespace) - tt.setupMocks(setup) - - result, err := setup.rs.Encrypt(setup.ctx, setup.repo, "test-secret", "secret-data") - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} - -func TestRepositorySecrets_Decrypt(t *testing.T) { - tests := []struct { - name string - namespace string - nameOrValue string - setupMocks func(*testSetup) - expectedResult []byte - expectedError string - }{ - { - name: "new service success - name starts with repo name", - namespace: "test-namespace", - nameOrValue: "test-repo-secret-name", - setupMocks: func(s *testSetup) { - s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte("decrypted-data"), nil) - }, - expectedResult: []byte("decrypted-data"), - }, - { - name: "new service error - name starts with repo name", - namespace: "test-namespace", - nameOrValue: "test-repo-secret-name", - setupMocks: func(s *testSetup) { - s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return(nil, errors.New("new service failed")) - }, - expectedError: "new service failed", - }, - { - name: "legacy service success - name does not start with repo name", - namespace: "test-namespace", - nameOrValue: "legacy-encrypted-value", - setupMocks: func(s *testSetup) { - s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte("decrypted-legacy-data"), nil) - }, - expectedResult: []byte("decrypted-legacy-data"), - }, - { - name: "legacy service error - name does not start with repo name", - namespace: "test-namespace", - nameOrValue: "legacy-encrypted-value", - setupMocks: func(s *testSetup) { - s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return(nil, errors.New("legacy service failed")) - }, - expectedError: "legacy service failed", - }, - { - name: "new service empty bytes - name starts with repo name", - namespace: "test-namespace", - nameOrValue: "test-repo-secret-name", - setupMocks: func(s *testSetup) { - s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte{}, nil) - }, - expectedResult: []byte{}, - }, - { - name: "legacy service empty bytes - name does not start with repo name", - namespace: "test-namespace", - nameOrValue: "legacy-encrypted-value", - setupMocks: func(s *testSetup) { - s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte{}, nil) - }, - expectedResult: []byte{}, - }, - { - name: "custom namespace handling - name starts with repo name", - namespace: "custom-namespace", - nameOrValue: "test-repo-secret-name", - setupMocks: func(s *testSetup) { - s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "test-repo-secret-name").Return([]byte("test-data"), nil) - }, - expectedResult: []byte("test-data"), - }, - { - name: "exact repo name match - should use new service", - namespace: "test-namespace", - nameOrValue: "test-repo", - setupMocks: func(s *testSetup) { - s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo").Return([]byte("exact-match-data"), nil) - }, - expectedResult: []byte("exact-match-data"), - }, - { - name: "partial repo name match - should use legacy service", - namespace: "test-namespace", - nameOrValue: "test-rep", - setupMocks: func(s *testSetup) { - s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("test-rep")).Return([]byte("partial-match-data"), nil) - }, - expectedResult: []byte("partial-match-data"), - }, - { - name: "empty name - should use legacy service", - namespace: "test-namespace", - nameOrValue: "", - setupMocks: func(s *testSetup) { - s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("")).Return([]byte("empty-name-data"), nil) - }, - expectedResult: []byte("empty-name-data"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setup := setupTest(t, tt.namespace) - tt.setupMocks(setup) - - result, err := setup.rs.Decrypt(setup.ctx, setup.repo, tt.nameOrValue) - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} - -func TestRepositorySecrets_Delete(t *testing.T) { - tests := []struct { - name string - namespace string - featureEnabled bool - setupMocks func(*testSetup) - expectedError string - }{ - { - name: "new service delete success", - namespace: "test-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(nil) - }, - }, - { - name: "new service delete error", - namespace: "test-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(errors.New("delete failed")) - }, - expectedError: "delete failed", - }, - { - name: "new service secret not found - should succeed", - namespace: "test-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(true) - s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "non-existent-secret").Return(contracts.ErrSecureValueNotFound) - }, - }, - { - name: "nothing for legacy", - namespace: "custom-namespace", - featureEnabled: true, - setupMocks: func(s *testSetup) { - s.expectFeatureFlag(false) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setup := setupTest(t, tt.namespace) - tt.setupMocks(setup) - - secretName := "secret-to-delete" - if tt.name == "new service secret not found - should succeed" { - secretName = "non-existent-secret" - } - err := setup.rs.Delete(setup.ctx, setup.repo, secretName) - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - } else { - assert.NoError(t, err) - } - - setup.mockSecrets.AssertExpectations(t) - }) - } -} diff --git a/pkg/registry/apis/provisioning/secrets/secret.go b/pkg/registry/apis/provisioning/secrets/secret.go deleted file mode 100644 index 19131b93765..00000000000 --- a/pkg/registry/apis/provisioning/secrets/secret.go +++ /dev/null @@ -1,165 +0,0 @@ -package secrets - -import ( - "context" - "errors" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" -) - -const svcName = provisioning.GROUP - -//go:generate mockery --name SecureValueClient --structname MockSecureValueClient --inpackage --filename secure_value_client_mock.go --with-expecter -type SecureValueClient = secret.SecureValueClient - -//go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter -type Service interface { - Encrypt(ctx context.Context, namespace, name string, data string) (string, error) - Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) - Delete(ctx context.Context, namespace string, name string) error -} - -var _ Service = (*secretsService)(nil) - -//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret --filename decrypt_service_mock.go --with-expecter -type secretsService struct { - secureValues SecureValueClient - decryptSvc secret.DecryptService - decrypterServiceName string -} - -func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService, grpcGrafanaServiceName string) Service { - return &secretsService{ - secureValues: secretsSvc, - decryptSvc: decryptSvc, - decrypterServiceName: grpcGrafanaServiceName, - } -} - -func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, data string) (string, error) { - client, err := s.secureValues.Client(ctx, namespace) - if err != nil { - return "", err - } - - // Try to get existing secret - existingUnstructured, err := client.Get(ctx, name, metav1.GetOptions{}) - if err != nil { - // If secret doesn't exist (not found error), we'll create it - // For other errors, return the error - if !errors.Is(err, contracts.ErrSecureValueNotFound) { - // Check if it's a k8s not found error - if !isNotFoundError(err) { - return "", err - } - } - } - - if existingUnstructured != nil { - // Update the value directly in the unstructured object - if err := unstructured.SetNestedField(existingUnstructured.Object, data, "spec", "value"); err != nil { - return "", err - } - - // Update using dynamic client - result, err := client.Update(ctx, existingUnstructured, metav1.UpdateOptions{}) - if err != nil { - return "", err - } - - return result.GetName(), nil - } - - decrypters := []string{svcName} - if s.decrypterServiceName != "" { - decrypters = append(decrypters, s.decrypterServiceName) - } - - // Create the secret directly as unstructured - secret := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "secret.grafana.app/v1beta1", - "kind": "SecureValue", - "metadata": map[string]interface{}{ - "namespace": namespace, - "name": name, - }, - "spec": map[string]interface{}{ - "description": "provisioning: " + name, - "value": data, - "decrypters": decrypters, - }, - }, - } - - // Create new secret - finalSecret, err := client.Create(ctx, secret, metav1.CreateOptions{}) - if err != nil { - return "", err - } - - return finalSecret.GetName(), nil -} - -func (s *secretsService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { - results, err := s.decryptSvc.Decrypt(ctx, svcName, namespace, name) - if err != nil { - return nil, err - } - - if res, ok := results[name]; ok { - if res.Error() == nil { - return []byte(res.Value().DangerouslyExposeAndConsumeValue()), nil - } - - return nil, res.Error() - } - - return nil, contracts.ErrDecryptNotFound -} - -func (s *secretsService) Delete(ctx context.Context, namespace string, name string) error { - client, err := s.secureValues.Client(ctx, namespace) - if err != nil { - return err - } - - if err := client.Delete(ctx, name, metav1.DeleteOptions{}); err != nil { - // FIXME: This is a temporary workaround until the client abstraction properly handles - // k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound - if isNotFoundError(err) { - return contracts.ErrSecureValueNotFound - } - return err - } - - return nil -} - -// Helper function to check if error is a not found error -// FIXME: This is a temporary workaround until the client abstraction properly handles -// k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound -func isNotFoundError(err error) bool { - if err == nil { - return false - } - - // Check for Grafana's secure value not found error - if errors.Is(err, contracts.ErrSecureValueNotFound) { - return true - } - - // Check for k8s not found error - if apierrors.IsNotFound(err) { - return true - } - - // Fallback for generic not found error messages - return err.Error() == "not found" -} diff --git a/pkg/registry/apis/provisioning/secrets/secret_mock.go b/pkg/registry/apis/provisioning/secrets/secret_mock.go deleted file mode 100644 index 3366e8e1255..00000000000 --- a/pkg/registry/apis/provisioning/secrets/secret_mock.go +++ /dev/null @@ -1,203 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package secrets - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockService is an autogenerated mock type for the Service type -type MockService struct { - mock.Mock -} - -type MockService_Expecter struct { - mock *mock.Mock -} - -func (_m *MockService) EXPECT() *MockService_Expecter { - return &MockService_Expecter{mock: &_m.Mock} -} - -// Decrypt provides a mock function with given fields: ctx, namespace, name -func (_m *MockService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { - ret := _m.Called(ctx, namespace, name) - - if len(ret) == 0 { - panic("no return value specified for Decrypt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok { - return rf(ctx, namespace, name) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok { - r0 = rf(ctx, namespace, name) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, namespace, name) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' -type MockService_Decrypt_Call struct { - *mock.Call -} - -// Decrypt is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - name string -func (_e *MockService_Expecter) Decrypt(ctx interface{}, namespace interface{}, name interface{}) *MockService_Decrypt_Call { - return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, namespace, name)} -} - -func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Decrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *MockService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockService_Decrypt_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockService_Decrypt_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, namespace, name -func (_m *MockService) Delete(ctx context.Context, namespace string, name string) error { - ret := _m.Called(ctx, namespace, name) - - 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) error); ok { - r0 = rf(ctx, namespace, name) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockService_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - name string -func (_e *MockService_Expecter) Delete(ctx interface{}, namespace interface{}, name interface{}) *MockService_Delete_Call { - return &MockService_Delete_Call{Call: _e.mock.On("Delete", ctx, namespace, name)} -} - -func (_c *MockService_Delete_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *MockService_Delete_Call) Return(_a0 error) *MockService_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockService_Delete_Call) RunAndReturn(run func(context.Context, string, string) error) *MockService_Delete_Call { - _c.Call.Return(run) - return _c -} - -// Encrypt provides a mock function with given fields: ctx, namespace, name, data -func (_m *MockService) Encrypt(ctx context.Context, namespace string, name string, data string) (string, error) { - ret := _m.Called(ctx, namespace, name, data) - - if len(ret) == 0 { - panic("no return value specified for Encrypt") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok { - return rf(ctx, namespace, name, data) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok { - r0 = rf(ctx, namespace, name, data) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, namespace, name, data) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' -type MockService_Encrypt_Call struct { - *mock.Call -} - -// Encrypt is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - name string -// - data string -func (_e *MockService_Expecter) Encrypt(ctx interface{}, namespace interface{}, name interface{}, data interface{}) *MockService_Encrypt_Call { - return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, namespace, name, data)} -} - -func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, namespace string, name string, data string)) *MockService_Encrypt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockService_Encrypt_Call) Return(_a0 string, _a1 error) *MockService_Encrypt_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, string, string, string) (string, error)) *MockService_Encrypt_Call { - _c.Call.Return(run) - return _c -} - -// NewMockService creates a new instance of MockService. 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 NewMockService(t interface { - mock.TestingT - Cleanup(func()) -}) *MockService { - mock := &MockService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/secrets/secret_test.go b/pkg/registry/apis/provisioning/secrets/secret_test.go deleted file mode 100644 index 9fccd4cebbe..00000000000 --- a/pkg/registry/apis/provisioning/secrets/secret_test.go +++ /dev/null @@ -1,573 +0,0 @@ -package secrets - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - 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/runtime/schema" - "k8s.io/client-go/dynamic" - - secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" - "github.com/grafana/grafana/pkg/registry/apis/secret" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" -) - -// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface -type mockDynamicInterface struct { - dynamic.ResourceInterface - getResult *unstructured.Unstructured - getErr error - createResult *unstructured.Unstructured - createErr error - updateResult *unstructured.Unstructured - updateErr error - deleteErr error -} - -func (m *mockDynamicInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { - return m.getResult, m.getErr -} - -func (m *mockDynamicInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { - return m.createResult, m.createErr -} - -func (m *mockDynamicInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { - return m.updateResult, m.updateErr -} - -func (m *mockDynamicInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { - return m.deleteErr -} - -func TestNewSecretsService(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - - assert.NotNil(t, svc) - assert.IsType(t, &secretsService{}, svc) -} - -//nolint:gocyclo // This test is complex but it's a good test for the SecretsService. -func TestSecretsService_Encrypt(t *testing.T) { - tests := []struct { - name string - namespace string - secretName string - data string - setupMocks func(*MockSecureValueClient, *secret.MockDecryptService, *mockDynamicInterface) - expectedName string - expectedError string - }{ - { - name: "successfully create new secret", - namespace: "test-namespace", - secretName: "test-secret", - data: "secret-data", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return not found error (secret doesn't exist) - mockResourceInterface.getResult = nil - mockResourceInterface.getErr = contracts.ErrSecureValueNotFound - - // Mock Create call - mockResourceInterface.createResult = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "test-secret", - "namespace": "test-namespace", - }, - }, - } - mockResourceInterface.createErr = nil - }, - expectedName: "test-secret", - }, - { - name: "successfully update existing secret", - namespace: "test-namespace", - secretName: "existing-secret", - data: "new-secret-data", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - existingSecret := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "existing-secret", - "namespace": "test-namespace", - }, - "spec": map[string]interface{}{ - "description": "provisioning: existing-secret", - "decrypters": []string{svcName}, - }, - }, - } - - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return existing secret - mockResourceInterface.getResult = existingSecret - mockResourceInterface.getErr = nil - - // Mock Update call - mockResourceInterface.updateResult = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "existing-secret", - "namespace": "test-namespace", - }, - }, - } - mockResourceInterface.updateErr = nil - }, - expectedName: "existing-secret", - }, - { - name: "error reading existing secret", - namespace: "test-namespace", - secretName: "test-secret", - data: "secret-data", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return error - mockResourceInterface.getResult = nil - mockResourceInterface.getErr = errors.New("database error") - }, - expectedError: "database error", - }, - { - name: "error creating new secret", - namespace: "test-namespace", - secretName: "test-secret", - data: "secret-data", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return not found error - mockResourceInterface.getResult = nil - mockResourceInterface.getErr = contracts.ErrSecureValueNotFound - - // Mock Create call to return error - mockResourceInterface.createResult = nil - mockResourceInterface.createErr = errors.New("creation failed") - }, - expectedError: "creation failed", - }, - { - name: "error updating existing secret", - namespace: "test-namespace", - secretName: "existing-secret", - data: "new-secret-data", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - existingSecret := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "existing-secret", - "namespace": "test-namespace", - }, - "spec": map[string]interface{}{ - "description": "provisioning: existing-secret", - "decrypters": []string{svcName}, - }, - }, - } - - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return existing secret - mockResourceInterface.getResult = existingSecret - mockResourceInterface.getErr = nil - - // Mock Update call to return error - mockResourceInterface.updateResult = nil - mockResourceInterface.updateErr = errors.New("update failed") - }, - expectedError: "update failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - mockResourceInterface := &mockDynamicInterface{} - - tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface) - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - - ctx := context.Background() - - result, err := svc.Encrypt(ctx, tt.namespace, tt.secretName, tt.data) - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedName, result) - } - }) - } -} - -func TestSecretsService_Encrypt_ClientError(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - - // Setup client to return error - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error")) - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - - ctx := context.Background() - - result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "client error") - assert.Empty(t, result) -} - -func TestSecretsService_Decrypt(t *testing.T) { - tests := []struct { - name string - namespace string - secretName string - setupMocks func(*MockSecureValueClient, *secret.MockDecryptService) - expectedResult []byte - expectedError string - }{ - { - name: "successfully decrypt secret", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) { - exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data") - mockResult := secret.NewDecryptResultValue(&exposedValue) - - mockDecryptSvc.EXPECT().Decrypt( - mock.MatchedBy(func(ctx context.Context) bool { - // Verify that the context is not nil (the service creates a new StaticRequester) - return ctx != nil - }), - svcName, - "test-namespace", - "test-secret", - ).Return(map[string]secret.DecryptResult{ - "test-secret": mockResult, - }, nil) - }, - expectedResult: []byte("decrypted-data"), - }, - { - name: "decrypt service error", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) { - mockDecryptSvc.EXPECT().Decrypt( - mock.MatchedBy(func(ctx context.Context) bool { - return ctx != nil - }), - svcName, - "test-namespace", - "test-secret", - ).Return(nil, errors.New("decrypt service error")) - }, - expectedError: "decrypt service error", - }, - { - name: "secret not found in results", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) { - mockDecryptSvc.EXPECT().Decrypt( - mock.MatchedBy(func(ctx context.Context) bool { - return ctx != nil - }), - svcName, - "test-namespace", - "test-secret", - ).Return(map[string]secret.DecryptResult{}, nil) - }, - expectedError: secret.ErrDecryptNotFound.Error(), - }, - { - name: "decrypt result has error", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) { - mockResult := secret.NewDecryptResultErr(errors.New("decryption failed")) - - mockDecryptSvc.EXPECT().Decrypt( - mock.MatchedBy(func(ctx context.Context) bool { - return ctx != nil - }), - svcName, - "test-namespace", - "test-secret", - ).Return(map[string]secret.DecryptResult{ - "test-secret": mockResult, - }, nil) - }, - expectedError: "decryption failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - - tt.setupMocks(mockSecretsSvc, mockDecryptSvc) - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - - ctx := context.Background() - - result, err := svc.Decrypt(ctx, tt.namespace, tt.secretName) - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} - -// Test to verify that the Decrypt method creates the correct service identity context -func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - - exposedValue := secretv1beta1.NewExposedSecureValue("test-data") - mockResult := secret.NewDecryptResultValue(&exposedValue) - - // Create a more detailed context matcher to verify the service identity context is created correctly - mockDecryptSvc.EXPECT().Decrypt( - mock.MatchedBy(func(ctx context.Context) bool { - // At minimum, verify the context is not nil and is different from the original - return ctx != nil - }), - svcName, - "test-namespace", - "test-secret", - ).Return(map[string]secret.DecryptResult{ - "test-secret": mockResult, - }, nil) - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - - ctx := context.Background() - result, err := svc.Decrypt(ctx, "test-namespace", "test-secret") - - assert.NoError(t, err) - assert.Equal(t, []byte("test-data"), result) -} - -func TestSecretsService_Delete(t *testing.T) { - tests := []struct { - name string - namespace string - secretName string - setupMocks func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) - expectedError string - }{ - { - name: "delete success", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Delete call - mockResourceInterface.deleteErr = nil - }, - }, - { - name: "delete returns error", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Delete call to return error - mockResourceInterface.deleteErr = errors.New("delete failed") - }, - expectedError: "delete failed", - }, - { - name: "client error", - namespace: "test-namespace", - secretName: "test-secret", - setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) { - // Setup client to return error - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error")) - }, - expectedError: "client error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - mockResourceInterface := &mockDynamicInterface{} - - tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface) - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - ctx := context.Background() - - err := svc.Delete(ctx, tt.namespace, tt.secretName) - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestIsNotFoundError(t *testing.T) { - tests := []struct { - name string - err error - expected bool - }{ - { - name: "nil error", - err: nil, - expected: false, - }, - { - name: "grafana secure value not found error", - err: contracts.ErrSecureValueNotFound, - expected: true, - }, - { - name: "k8s not found error", - err: apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret"), - expected: true, - }, - { - name: "generic not found error message", - err: errors.New("not found"), - expected: true, - }, - { - name: "other error", - err: errors.New("internal server error"), - expected: false, - }, - { - name: "wrapped grafana error", - err: errors.New("wrapped: " + contracts.ErrSecureValueNotFound.Error()), - expected: false, // wrapped errors won't match errors.Is unless properly wrapped - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := isNotFoundError(tt.err) - assert.Equal(t, tt.expected, result, "isNotFoundError(%v) = %v, want %v", tt.err, result, tt.expected) - }) - } -} - -func TestSecretsService_Encrypt_WithK8sNotFoundError(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - mockResourceInterface := &mockDynamicInterface{} - - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Get call to return k8s not found error - k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret") - mockResourceInterface.getResult = nil - mockResourceInterface.getErr = k8sNotFoundErr - - // Mock Create call to succeed - mockResourceInterface.createResult = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "test-secret", - "namespace": "test-namespace", - }, - }, - } - mockResourceInterface.createErr = nil - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - ctx := context.Background() - - result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") - - assert.NoError(t, err) - assert.Equal(t, "test-secret", result) -} - -func TestSecretsService_Delete_WithK8sNotFoundError(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - mockResourceInterface := &mockDynamicInterface{} - - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Delete call to return k8s not found error - k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret") - mockResourceInterface.deleteErr = k8sNotFoundErr - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - ctx := context.Background() - - err := svc.Delete(ctx, "test-namespace", "test-secret") - - // Should return contracts.ErrSecureValueNotFound instead of k8s error - assert.Error(t, err) - assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound) -} - -func TestSecretsService_Delete_WithGrafanaNotFoundError(t *testing.T) { - mockSecretsSvc := NewMockSecureValueClient(t) - mockDecryptSvc := &secret.MockDecryptService{} - mockResourceInterface := &mockDynamicInterface{} - - // Setup client to return the mock resource interface - mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil) - - // Mock Delete call to return Grafana not found error - mockResourceInterface.deleteErr = contracts.ErrSecureValueNotFound - - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "") - ctx := context.Background() - - err := svc.Delete(ctx, "test-namespace", "test-secret") - - // Should return contracts.ErrSecureValueNotFound - assert.Error(t, err) - assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound) -} diff --git a/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go b/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go deleted file mode 100644 index 25b9a4ca3b8..00000000000 --- a/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go +++ /dev/null @@ -1,97 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package secrets - -import ( - context "context" - - dynamic "k8s.io/client-go/dynamic" - - mock "github.com/stretchr/testify/mock" -) - -// MockSecureValueClient is an autogenerated mock type for the SecureValueClient type -type MockSecureValueClient struct { - mock.Mock -} - -type MockSecureValueClient_Expecter struct { - mock *mock.Mock -} - -func (_m *MockSecureValueClient) EXPECT() *MockSecureValueClient_Expecter { - return &MockSecureValueClient_Expecter{mock: &_m.Mock} -} - -// Client provides a mock function with given fields: ctx, namespace -func (_m *MockSecureValueClient) Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) { - ret := _m.Called(ctx, namespace) - - if len(ret) == 0 { - panic("no return value specified for Client") - } - - var r0 dynamic.ResourceInterface - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (dynamic.ResourceInterface, error)); ok { - return rf(ctx, namespace) - } - if rf, ok := ret.Get(0).(func(context.Context, string) dynamic.ResourceInterface); ok { - r0 = rf(ctx, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(dynamic.ResourceInterface) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, namespace) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockSecureValueClient_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client' -type MockSecureValueClient_Client_Call struct { - *mock.Call -} - -// Client is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -func (_e *MockSecureValueClient_Expecter) Client(ctx interface{}, namespace interface{}) *MockSecureValueClient_Client_Call { - return &MockSecureValueClient_Client_Call{Call: _e.mock.On("Client", ctx, namespace)} -} - -func (_c *MockSecureValueClient_Client_Call) Run(run func(ctx context.Context, namespace string)) *MockSecureValueClient_Client_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *MockSecureValueClient_Client_Call) Return(_a0 dynamic.ResourceInterface, _a1 error) *MockSecureValueClient_Client_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockSecureValueClient_Client_Call) RunAndReturn(run func(context.Context, string) (dynamic.ResourceInterface, error)) *MockSecureValueClient_Client_Call { - _c.Call.Return(run) - return _c -} - -// NewMockSecureValueClient creates a new instance of MockSecureValueClient. 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 NewMockSecureValueClient(t interface { - mock.TestingT - Cleanup(func()) -}) *MockSecureValueClient { - mock := &MockSecureValueClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/test.go b/pkg/registry/apis/provisioning/test.go index c180d70c7c5..89733b26b32 100644 --- a/pkg/registry/apis/provisioning/test.go +++ b/pkg/registry/apis/provisioning/test.go @@ -89,22 +89,31 @@ func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.O // HACK: Set the name and namespace if not set so that the temporary repository can be created // This can be removed once we deprecate legacy secrets is deprecated or we use InLineSecureValues as we // use the same field and repository name to detect which one to use. - if cfg.GetName() == "" { - if name == "new" { - // HACK: frontend is passing a "new" we need to remove the hack there as well - // Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test" - name = "hack-on-hack-for-new" + if name == "new" { + // HACK: frontend is passing a "new" we need to remove the hack there as well + // Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test" + name = "hack-on-hack-for-new" + } else { + // Copy previous secure values if they exist + old, _ := s.getter.GetRepository(ctx, name) + if old != nil && !old.Config().Secure.IsZero() { + secure := old.Config().Secure + if cfg.Secure.Token.IsZero() { + cfg.Secure.Token = secure.Token + } + if cfg.Secure.WebhookSecret.IsZero() { + cfg.Secure.WebhookSecret = secure.WebhookSecret + } } - - cfg.SetName(name) } + cfg.SetName(name) if cfg.GetNamespace() == "" { cfg.SetNamespace(ns) } // Create a temporary repository - tmp, err := s.getter.AsRepository(ctx, &cfg) + tmp, err := s.getter.RepositoryFromConfig(ctx, &cfg) if err != nil { responder.Error(err) return diff --git a/pkg/registry/apis/provisioning/types.go b/pkg/registry/apis/provisioning/types.go index 8c030b652e1..9f3db292b0b 100644 --- a/pkg/registry/apis/provisioning/types.go +++ b/pkg/registry/apis/provisioning/types.go @@ -18,7 +18,7 @@ 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) + RepositoryFromConfig(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error) } type ClientGetter interface { diff --git a/pkg/registry/apis/provisioning/webhooks/mutator.go b/pkg/registry/apis/provisioning/webhooks/mutator.go deleted file mode 100644 index b93677228b0..00000000000 --- a/pkg/registry/apis/provisioning/webhooks/mutator.go +++ /dev/null @@ -1,35 +0,0 @@ -package webhooks - -import ( - "context" - - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "k8s.io/apimachinery/pkg/runtime" -) - -func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { - return func(ctx context.Context, obj runtime.Object) error { - repo, ok := obj.(*provisioning.Repository) - if !ok { - return nil - } - - if repo.Status.Webhook == nil { - return nil - } - - if repo.Status.Webhook.Secret != "" { - secretName := repo.Name + webhookSecretSuffix - nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Status.Webhook.Secret) - if err != nil { - return err - } - repo.Status.Webhook.EncryptedSecret = nameOrValue - repo.Status.Webhook.Secret = "" - } - - return nil - } -} diff --git a/pkg/registry/apis/provisioning/webhooks/mutator_test.go b/pkg/registry/apis/provisioning/webhooks/mutator_test.go deleted file mode 100644 index 209d95d5284..00000000000 --- a/pkg/registry/apis/provisioning/webhooks/mutator_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package webhooks - -import ( - "context" - "errors" - "testing" - - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -func TestMutator(t *testing.T) { - tests := []struct { - name string - obj runtime.Object - secret string - setupMocks func(*secrets.MockRepositorySecrets) - expectedEncryptedSecret string - expectedError string - }{ - { - name: "successful secret encryption", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - Secret: "webhook-secret", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - Secret: "webhook-secret", - }, - }, - }, - "test-repo-webhook-secret", - "webhook-secret", - ).Return([]byte("encrypted-webhook-secret"), nil) - }, - expectedEncryptedSecret: "encrypted-webhook-secret", - }, - { - name: "encryption error", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - Secret: "webhook-secret", - }, - }, - }, - setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Encrypt( - context.Background(), - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - Secret: "webhook-secret", - }, - }, - }, - "test-repo-webhook-secret", - "webhook-secret", - ).Return(nil, errors.New("encryption failed")) - }, - expectedError: "encryption failed", - }, - { - name: "no webhook status", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: nil, - }, - }, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, - }, - { - name: "empty secret", - obj: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - Secret: "", - }, - }, - }, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, - }, - { - name: "non-repository object", - obj: &runtime.Unknown{}, - setupMocks: func(_ *secrets.MockRepositorySecrets) { - // No expectations - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMocks(mockSecrets) - - mutator := Mutator(mockSecrets) - err := mutator(context.Background(), tt.obj) - - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - } else { - assert.NoError(t, err) - - // Check that secret was cleared and encrypted secret was set - if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Status.Webhook != nil { - if tt.expectedEncryptedSecret != "" { - // Secret should be cleared after encryption - assert.Empty(t, repo.Status.Webhook.Secret, "Secret should be cleared after encryption") - // EncryptedSecret should be set to the expected value - assert.Equal(t, tt.expectedEncryptedSecret, string(repo.Status.Webhook.EncryptedSecret), "EncryptedSecret should match expected value") - } - } - } - }) - } -} diff --git a/pkg/registry/apis/provisioning/webhooks/register.go b/pkg/registry/apis/provisioning/webhooks/register.go index 7fa6550f15e..55bb26e02f5 100644 --- a/pkg/registry/apis/provisioning/webhooks/register.go +++ b/pkg/registry/apis/provisioning/webhooks/register.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "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/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -47,7 +46,6 @@ func isPublicURL(url string) bool { func ProvideWebhooks( cfg *setting.Cfg, features featuremgmt.FeatureToggles, - repositorySecrets secrets.RepositorySecrets, ghFactory *github.Factory, renderer rendering.Service, blobstore resource.ResourceClient, @@ -79,7 +77,6 @@ func ProvideWebhooks( render, webhook, urlProvider, - repositorySecrets, ghFactory, parsers, []jobs.Worker{pullRequestWorker}, @@ -95,7 +92,6 @@ type WebhookExtra struct { render *renderConnector webhook *webhookConnector urlProvider func(namespace string) string - secrets secrets.RepositorySecrets ghFactory *github.Factory parsers resources.ParserFactory workers []jobs.Worker @@ -106,7 +102,6 @@ func NewWebhookExtra( render *renderConnector, webhook *webhookConnector, urlProvider func(namespace string) string, - secrets secrets.RepositorySecrets, ghFactory *github.Factory, parsers resources.ParserFactory, workers []jobs.Worker, @@ -116,7 +111,6 @@ func NewWebhookExtra( render: render, webhook: webhook, urlProvider: urlProvider, - secrets: secrets, ghFactory: ghFactory, parsers: parsers, workers: workers, @@ -136,9 +130,7 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) ( // Mutators returns the mutators for the webhook extra func (e *WebhookExtra) Mutators() []controller.Mutator { - return []controller.Mutator{ - Mutator(e.secrets), - } + return nil } // UpdateStorage updates the storage with both render and webhook connectors @@ -164,7 +156,7 @@ func (e *WebhookExtra) GetJobWorkers() []jobs.Worker { } // AsRepository delegates repository creation to the webhook connector -func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) { +func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error) { // Only handle GitHub repositories with webhooks if URL is public if r.Spec.Type == provisioning.GitHubRepositoryType && e.isPublic { gvr := provisioning.RepositoryResourceInfo.GroupVersionResource() @@ -186,34 +178,33 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit } // Decrypt GitHub token if needed - ghToken := ghCfg.Token - if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { - decrypted, err := e.secrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) - if err != nil { - return nil, fmt.Errorf("decrypt github token: %w", err) - } - ghToken = string(decrypted) + ghToken, err := secure.Token(ctx) + if err != nil { + return nil, fmt.Errorf("decrypt github token: %w", err) + } + webhookSecret, err := secure.WebhookSecret(ctx) + if err != nil { + return nil, fmt.Errorf("decrypt webhookSecret: %w", err) } gitCfg := git.RepositoryConfig{ - URL: ghCfg.URL, - Branch: ghCfg.Branch, - Path: ghCfg.Path, - Token: ghToken, - EncryptedToken: ghCfg.EncryptedToken, + URL: ghCfg.URL, + Branch: ghCfg.Branch, + Path: ghCfg.Path, + Token: ghToken, } - gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, e.secrets) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg) if err != nil { return nil, fmt.Errorf("error creating git repository: %w", err) } - basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken, e.secrets) + basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken) if err != nil { return nil, fmt.Errorf("error creating github repository: %w", err) } - return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil + return NewGithubWebhookRepository(basicRepo, webhookURL, webhookSecret), nil } return nil, nil diff --git a/pkg/registry/apis/provisioning/webhooks/repository.go b/pkg/registry/apis/provisioning/webhooks/repository.go index c122bdba2f4..3359a568350 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository.go +++ b/pkg/registry/apis/provisioning/webhooks/repository.go @@ -10,18 +10,16 @@ import ( "github.com/google/go-github/v70/github" "github.com/google/uuid" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -var subscribedEvents = []string{"push", "pull_request"} - -//nolint:gosec // This is a constant for a secret suffix -const webhookSecretSuffix = "-webhook-secret" +var subscribedEvents = []string{"pull_request", "push"} // same order as slices.Sort() type WebhookRepository interface { Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error) @@ -39,7 +37,7 @@ type githubWebhookRepository struct { config *provisioning.Repository owner string repo string - secrets secrets.RepositorySecrets + secret common.RawSecureValue gh pgh.Client webhookURL string } @@ -47,7 +45,7 @@ type githubWebhookRepository struct { func NewGithubWebhookRepository( basic pgh.GithubRepository, webhookURL string, - secrets secrets.RepositorySecrets, + secret common.RawSecureValue, ) GithubWebhookRepository { return &githubWebhookRepository{ GithubRepository: basic, @@ -56,7 +54,7 @@ func NewGithubWebhookRepository( repo: basic.Repo(), gh: basic.Client(), webhookURL: webhookURL, - secrets: secrets, + secret: secret, } } @@ -66,12 +64,11 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request return nil, fmt.Errorf("unexpected webhook request") } - secret, err := r.secrets.Decrypt(ctx, r.config, string(r.config.Status.Webhook.EncryptedSecret)) - if err != nil { - return nil, fmt.Errorf("failed to decrypt secret: %w", err) + if r.secret.IsZero() { + return nil, fmt.Errorf("missing webhook secret") } - payload, err := github.ValidatePayload(req, secret) + payload, err := github.ValidatePayload(req, []byte(r.secret)) if err != nil { return nil, apierrors.NewUnauthorized("invalid signature") } @@ -239,8 +236,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo 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.webhookURL { @@ -248,6 +243,7 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo hook.URL = r.webhookURL } + slices.Sort(hook.Events) // consistent order for comparison if !slices.Equal(hook.Events, subscribedEvents) { mustUpdate = true hook.Events = subscribedEvents @@ -263,7 +259,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo 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) } @@ -304,10 +299,16 @@ func (r *githubWebhookRepository) OnCreate(ctx context.Context) ([]map[string]in "value": &provisioning.WebhookStatus{ ID: hook.ID, URL: hook.URL, - Secret: hook.Secret, SubscribedEvents: hook.Events, }, }, + { + "op": "replace", + "path": "/secure/webhookSecret", + "value": map[string]string{ + "create": hook.Secret, + }, + }, }, nil } @@ -316,42 +317,34 @@ func (r *githubWebhookRepository) OnUpdate(ctx context.Context) ([]map[string]in return nil, nil } ctx, _ = r.logger(ctx, "") - hook, _, err := r.updateWebhook(ctx) - if err != nil { + hook, changed, err := r.updateWebhook(ctx) + if err != nil || !changed { return nil, err } - return []map[string]interface{}{ - { - "op": "replace", - "path": "/status/webhook", - "value": &provisioning.WebhookStatus{ - ID: hook.ID, - URL: hook.URL, - Secret: hook.Secret, - SubscribedEvents: hook.Events, - }, + // update the webhook and secret + return []map[string]any{{ + "op": "replace", + "path": "/status/webhook", + "value": &provisioning.WebhookStatus{ + ID: hook.ID, + URL: hook.URL, + SubscribedEvents: hook.Events, }, - }, nil + }, { + "op": "replace", + "path": "/secure/webhookSecret", + "value": map[string]string{ + "create": hook.Secret, + }, + }}, nil } func (r *githubWebhookRepository) OnDelete(ctx context.Context) error { - ctx, logger := r.logger(ctx, "") - if err := r.GithubRepository.OnDelete(ctx); err != nil { - return fmt.Errorf("on delete from basic github repository: %w", err) - } - if r.config.Status.Webhook == nil { return nil } - secretName := r.config.Name + webhookSecretSuffix - if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { - return fmt.Errorf("delete webhook secret: %w", err) - } - - logger.Info("Deleted webhook secret", "secretName", secretName) - return r.deleteWebhook(ctx) } diff --git a/pkg/registry/apis/provisioning/webhooks/repository_test.go b/pkg/registry/apis/provisioning/webhooks/repository_test.go index 38b561fe0eb..196b3276469 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository_test.go +++ b/pkg/registry/apis/provisioning/webhooks/repository_test.go @@ -14,13 +14,15 @@ import ( "strings" "testing" - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/google/uuid" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" ) func TestParseWebhooks(t *testing.T) { @@ -114,9 +116,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { tests := []struct { name string config *provisioning.Repository - webhookSecret string setupRequest func() *http.Request - mockSetup func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) expected *provisioning.WebhookResponse expectedError error }{ @@ -138,30 +138,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, expectedError: fmt.Errorf("unexpected webhook request"), }, - { - name: "secret decryption error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, - }, - }, - setupRequest: func() *http.Request { - req, _ := http.NewRequest("POST", "/webhook", nil) - return req - }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return(nil, errors.New("decryption failed")) - }, - expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), - }, { name: "invalid signature", config: &provisioning.Repository{ @@ -171,22 +147,15 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { req, _ := http.NewRequest("POST", "/webhook", strings.NewReader("invalid payload")) req.Header.Set("X-Hub-Signature-256", "invalid") req.Header.Set("Content-Type", "application/json") return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: apierrors.NewUnauthorized("invalid signature"), }, { @@ -198,12 +167,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{}` req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) @@ -218,10 +184,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, Message: "ping received", @@ -239,12 +201,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/feature", @@ -264,10 +223,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, }, @@ -287,12 +242,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/main", @@ -312,10 +264,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusAccepted, Job: &provisioning.JobSpec{ @@ -336,12 +284,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/main" @@ -358,10 +303,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("missing repository in push event"), }, { @@ -373,12 +314,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/main", @@ -398,10 +336,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("repository mismatch"), }, { @@ -416,12 +350,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/main", @@ -441,10 +372,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, }, @@ -461,12 +388,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -497,10 +421,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusAccepted, Message: "pull request: opened", @@ -528,12 +448,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "synchronize", @@ -564,10 +481,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusAccepted, Message: "pull request: synchronize", @@ -592,12 +505,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -628,10 +538,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, Message: "ignoring pull request event as develop is not the configured branch", @@ -646,12 +552,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "closed", @@ -682,10 +585,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, Message: "ignore pull request event: closed", @@ -700,12 +599,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -733,10 +629,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("missing repository in pull request event"), }, { @@ -746,12 +638,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { // GitHub config is intentionally missing }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -782,10 +671,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("missing GitHub config"), }, { @@ -797,12 +682,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -833,10 +715,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("repository mismatch"), }, { @@ -848,12 +726,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "action": "opened", @@ -873,39 +748,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expectedError: fmt.Errorf("expected PR in event"), }, - { - name: "secret decryption error with new secrets store", - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("test-secret"), - }, - }, - }, - setupRequest: func() *http.Request { - req, _ := http.NewRequest("POST", "/webhook", nil) - return req - }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). - Return(nil, errors.New("decryption failed")) - }, - expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), - }, { name: "ping event with new secrets store", config: &provisioning.Repository{ @@ -918,12 +762,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("test-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{}` req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) @@ -938,10 +779,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusOK, Message: "ping received", @@ -963,12 +800,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("test-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{ "ref": "refs/heads/main", @@ -988,10 +822,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusAccepted, Job: &provisioning.JobSpec{ @@ -1012,12 +842,9 @@ func TestGitHubRepository_Webhook(t *testing.T) { }, }, Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - EncryptedSecret: []byte("encrypted-secret"), - }, + Webhook: &provisioning.WebhookStatus{}, }, }, - webhookSecret: "webhook-secret", setupRequest: func() *http.Request { payload := `{}` req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) @@ -1032,10 +859,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { - mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). - Return([]byte("webhook-secret"), nil) - }, expected: &provisioning.WebhookResponse{ Code: http.StatusNotImplemented, Message: "unsupported messageType: team", @@ -1045,20 +868,12 @@ func TestGitHubRepository_Webhook(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create a mock secrets service - mockSecrets := secrets.NewMockRepositorySecrets(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockSecrets) - } - // Create a GitHub repository with the test config repo := &githubWebhookRepository{ - config: tt.config, - owner: "grafana", - repo: "grafana", - secrets: mockSecrets, + config: tt.config, + owner: "grafana", + repo: "grafana", + secret: common.RawSecureValue("webhook-secret"), } // Call the Webhook method @@ -1070,7 +885,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { var statusErr *apierrors.StatusError if errors.As(tt.expectedError, &statusErr) { var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") + require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type: %T", err) require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) } else { @@ -1097,9 +912,6 @@ func TestGitHubRepository_Webhook(t *testing.T) { require.Nil(t, response.Job) } } - - // Verify all mock expectations were met - mockSecrets.AssertExpectations(t) }) } } @@ -1202,9 +1014,8 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }, webhookURL: "https://example.com/webhook", expectedHook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - Secret: "test-secret", + ID: 123, + URL: "https://example.com/webhook", }, expectedError: nil, }, @@ -1270,12 +1081,21 @@ func TestGitHubRepository_OnCreate(t *testing.T) { require.NoError(t, err) if tt.expectedHook != nil { require.NotNil(t, hookOps) - require.Len(t, hookOps, 1) + require.Len(t, hookOps, 2) require.Equal(t, "replace", hookOps[0]["op"]) require.Equal(t, "/status/webhook", hookOps[0]["path"]) require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID) require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL) - require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty + + require.Equal(t, "replace", hookOps[1]["op"]) + require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"]) + vals, ok := hookOps[1]["value"].(map[string]string) + require.True(t, ok, "expected webhookSecret as map") + require.Len(t, vals, 1, "with one property") + require.NotEmpty(t, vals["create"], "secret should be created") + + _, err := uuid.Parse(vals["create"]) + require.NoError(t, err, "the secret is a valid UUID") } else { require.Nil(t, hookOps) } @@ -1607,7 +1427,6 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { URL: "https://example.com/webhook", Events: subscribedEvents, }, nil) - // No EditWebhook call expected since no changes needed }, config: &provisioning.Repository{ @@ -1618,19 +1437,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, Status: provisioning.RepositoryStatus{ Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - Secret: "secret", + ID: 123, + URL: "https://example.com/webhook", + }, + }, + Secure: provisioning.SecureValues{ + WebhookSecret: common.InlineSecureValue{ + Name: "valid-secret", }, }, }, - webhookURL: "https://example.com/webhook", - expectedHook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - SubscribedEvents: subscribedEvents, - Secret: "secret", - }, + webhookURL: "https://example.com/webhook", + expectedHook: nil, // nothing changed expectedError: nil, }, } @@ -1662,17 +1480,22 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { require.NoError(t, err) if tt.expectedHook != nil { require.NotNil(t, hookOps) - require.Len(t, hookOps, 1) + require.Len(t, hookOps, 2) require.Equal(t, "replace", hookOps[0]["op"]) require.Equal(t, "/status/webhook", hookOps[0]["path"]) require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID) require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL) - if tt.expectedHook.Secret != "" { - require.Equal(t, tt.expectedHook.Secret, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) - } else { - require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty - } require.ElementsMatch(t, tt.expectedHook.SubscribedEvents, hookOps[0]["value"].(*provisioning.WebhookStatus).SubscribedEvents) + + require.Equal(t, "replace", hookOps[1]["op"]) + require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"]) + vals, ok := hookOps[1]["value"].(map[string]string) + require.True(t, ok, "expected webhookSecret as map") + require.Len(t, vals, 1, "with one property") + require.NotEmpty(t, vals["create"], "secret should be created") + + _, err := uuid.Parse(vals["create"]) + require.NoError(t, err, "the secret is a valid UUID") } else { require.Nil(t, hookOps) } @@ -1687,17 +1510,14 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { func TestGitHubRepository_OnDelete(t *testing.T) { tests := []struct { name string - setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedError error }{ { name: "successfully delete webhook", - setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil) - // Mock deleting the webhook + setupMock: func(m *github.MockClient) { m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(nil) }, @@ -1721,10 +1541,8 @@ func TestGitHubRepository_OnDelete(t *testing.T) { expectedError: nil, }, { - name: "no webhook URL provided", - setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - }, + name: "no webhook URL provided", + setupMock: func(_ *github.MockClient) {}, config: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -1740,8 +1558,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, { name: "webhook not found in status", - setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) + setupMock: func(_ *github.MockClient) { // No secrets deletion or webhook deletion mocks needed - method returns early when webhook is nil }, config: &provisioning.Repository{ @@ -1760,35 +1577,9 @@ func TestGitHubRepository_OnDelete(t *testing.T) { webhookURL: "https://example.com/webhook", expectedError: nil, // No error expected - method returns early when webhook is nil }, - { - name: "error on delete from basic github repository", - setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(fmt.Errorf("failed to delete webhook")) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - }, - }, - }, - webhookURL: "https://example.com/webhook", - expectedError: fmt.Errorf("on delete from basic github repository: failed to delete webhook"), - }, { name: "error deleting webhook", - setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil) + setupMock: func(m *github.MockClient) { // Mock webhook deletion failure m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(fmt.Errorf("failed to delete webhook")) @@ -1819,15 +1610,13 @@ func TestGitHubRepository_OnDelete(t *testing.T) { // Setup mock GitHub client mockGH := github.NewMockClient(t) mockRepo := github.NewMockGithubRepository(t) - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMock(mockGH, mockRepo, mockSecrets) + tt.setupMock(mockGH) // Create repository with mock repo := &githubWebhookRepository{ GithubRepository: mockRepo, gh: mockGH, config: tt.config, - secrets: mockSecrets, owner: "grafana", repo: "grafana", webhookURL: tt.webhookURL, @@ -1849,156 +1638,3 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }) } } - -func TestGitHubRepository_OnDelete_WithSecrets(t *testing.T) { - tests := []struct { - name string - setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) - config *provisioning.Repository - webhookURL string - expectedError string - }{ - { - name: "successful deletion with secrets", - setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - mockSecrets.EXPECT().Delete( - mock.Anything, - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - }, - }, - }, - "test-repo"+webhookSecretSuffix, - ).Return(nil) - m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).Return(nil) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - }, - }, - }, - webhookURL: "https://example.com/webhook", - }, - { - name: "secret deletion error", - setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - mockSecrets.EXPECT().Delete( - mock.Anything, - &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - }, - }, - }, - "test-repo"+webhookSecretSuffix, - ).Return(errors.New("failed to delete webhook secret")) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - Status: provisioning.RepositoryStatus{ - Webhook: &provisioning.WebhookStatus{ - ID: 123, - URL: "https://example.com/webhook", - }, - }, - }, - webhookURL: "https://example.com/webhook", - expectedError: "delete webhook secret: failed to delete webhook secret", - }, - { - name: "no webhook URL - no secrets deletion", - setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { - mockRepo.On("OnDelete", mock.Anything).Return(nil) - }, - config: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "default", - }, - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - webhookURL: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := github.NewMockClient(t) - mockRepo := github.NewMockGithubRepository(t) - mockSecrets := secrets.NewMockRepositorySecrets(t) - tt.setupMock(mockClient, mockRepo, mockSecrets) - - repo := &githubWebhookRepository{ - GithubRepository: mockRepo, - gh: mockClient, - config: tt.config, - secrets: mockSecrets, - owner: "grafana", - repo: "grafana", - webhookURL: tt.webhookURL, - } - - err := repo.OnDelete(context.Background()) - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - } else { - require.NoError(t, err) - } - - mockClient.AssertExpectations(t) - mockRepo.AssertExpectations(t) - mockSecrets.AssertExpectations(t) - }) - } -} diff --git a/pkg/registry/apis/provisioning/webhooks/webhook.go b/pkg/registry/apis/provisioning/webhooks/webhook.go index bf239f31e24..c922ddfe3ee 100644 --- a/pkg/registry/apis/provisioning/webhooks/webhook.go +++ b/pkg/registry/apis/provisioning/webhooks/webhook.go @@ -172,7 +172,7 @@ func (s *webhookConnector) updateLastEvent(ctx context.Context, repo repository. eventAge := time.Since(lastEvent) if repo.Config().Status.Webhook != nil && (eventAge > time.Minute) { - patchOp := map[string]interface{}{ + patchOp := map[string]any{ "op": "replace", "path": "/status/webhook/lastEvent", "value": time.Now().UnixMilli(), diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index feb6975bc80..25bf6439908 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -125,7 +125,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O defer span.End() ctx = request.WithNamespace(ctx, request.NamespaceValue(connectCtx)) traceId := span.SpanContext().TraceID() - connectLogger := b.log.New("traceId", traceId.String()) + connectLogger := b.log.New("traceId", traceId.String(), "rule_uid", httpreq.Header.Get("X-Rule-Uid")) responder := newResponderWrapper(incomingResponder, func(statusCode *int, obj runtime.Object) { if *statusCode/100 == 4 { @@ -255,7 +255,7 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil instanceConfig := instance.GetSettings() - dsQuerierLoggerWithSlug := instance.GetLogger(connectLogger).New("ruleuid", headers["X-Rule-Uid"]) + dsQuerierLoggerWithSlug := instance.GetLogger(connectLogger) qsDsClientBuilder := dsquerierclient.NewQsDatasourceClientBuilderWithInstance( instance, diff --git a/pkg/registry/apis/secret/register.go b/pkg/registry/apis/secret/register.go index 60aa3896d50..bf556405e33 100644 --- a/pkg/registry/apis/secret/register.go +++ b/pkg/registry/apis/secret/register.go @@ -24,11 +24,6 @@ func RegisterDependencies( return nil, fmt.Errorf("registering access control roles: %w", err) } - // TODO: Remove once the DB schema is more stable. - if !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return nil, nil - } - // We shouldn't need to create the DB in HG, as that will use the MT api server. if cfg.StackID == "" { // Some DBs that claim to be MySQL/Postgres-compatible might not support table locking. diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 006d81b421c..acee2d9bef1 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" "github.com/grafana/grafana/pkg/registry/apis/provisioning" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -45,7 +44,6 @@ var WireSet = wire.NewSet( datasource.RegisterAPIService, folders.RegisterAPIService, iam.RegisterAPIService, - secrets.ProvideRepositorySecrets, provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 19cb24db04b..38e3c3c16e3 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -189,7 +189,7 @@ func (s *ModuleServer) Run() error { }) m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) { - return authz.ProvideZanzanaService(s.cfg, s.features) + return authz.ProvideZanzanaService(s.cfg, s.features, s.registerer) }) m.RegisterModule(modules.FrontendServer, func() (services.Service, error) { diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index e1659ebb460..502f1045b14 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -7,7 +7,6 @@ import ( "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/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -37,7 +36,6 @@ func ProvideTestEnv( idService auth.IDService, githubFactory *github.Factory, decryptService secret.DecryptService, - repositorySecrets secrets.RepositorySecrets, // TODO... remove ) (*TestEnv, error) { return &TestEnv{ TestingT: testingT, @@ -54,7 +52,6 @@ func ProvideTestEnv( IDService: idService, GitHubFactory: githubFactory, DecryptService: decryptService, - RepositorySecrets: repositorySecrets, // TODO, remove }, nil } @@ -77,5 +74,4 @@ type TestEnv struct { IDService auth.IDService GitHubFactory *github.Factory DecryptService secret.DecryptService - RepositorySecrets secrets.RepositorySecrets // NOTE, this will be removed soon } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 7b3c7db6b68..2b5d43803c4 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -60,7 +60,6 @@ import ( provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/extras" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" query2 "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" @@ -205,7 +204,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" - secrets2 "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" @@ -813,11 +812,9 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secureValueClient := secret.ProvideSecureValueClient(secureValueService, secureValueValidator, accessClient) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3, cfg) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, factory, renderingService, resourceClient, eventualRestConfigProvider) v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, v3, repositorySecrets, tracingService, v4) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, v3, tracingService, v4) if err != nil { return nil, err } @@ -1391,11 +1388,9 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secureValueClient := secret.ProvideSecureValueClient(secureValueService, secureValueValidator, accessClient) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3, cfg) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, factory, renderingService, resourceClient, eventualRestConfigProvider) v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, v3, repositorySecrets, tracingService, v4) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, v3, tracingService, v4) if err != nil { return nil, err } @@ -1430,7 +1425,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory, v3, repositorySecrets) + testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory, v3) if err != nil { return nil, err } @@ -1615,7 +1610,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets2.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets2.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator2.NewWithEngine, database4.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database4.Database)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator2.NewWithEngine, database4.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database4.Database)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 35ac04151f2..c42befe8d12 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -88,7 +88,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger, tracer) + srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -127,11 +127,12 @@ type ZanzanaService interface { var _ ZanzanaService = (*Zanzana)(nil) // ProvideZanzanaService is used to register zanzana as a module so we can run it seperatly from grafana. -func ProvideZanzanaService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*Zanzana, error) { +func ProvideZanzanaService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (*Zanzana, error) { s := &Zanzana{ cfg: cfg, features: features, logger: log.New("zanzana.server"), + reg: reg, } s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName("zanzana") @@ -147,6 +148,7 @@ type Zanzana struct { logger log.Logger handle grpcserver.Provider features featuremgmt.FeatureToggles + reg prometheus.Registerer } func (z *Zanzana) start(ctx context.Context) error { @@ -172,7 +174,7 @@ func (z *Zanzana) start(ctx context.Context) error { return fmt.Errorf("failed to start zanzana: %w", err) } - zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer) + zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 1e46aa52324..91a9800efbd 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -5,6 +5,7 @@ import ( openfgaserver "github.com/openfga/openfga/pkg/server" openfgastorage "github.com/openfga/openfga/pkg/storage" + "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -13,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger, tracer tracing.Tracer) (*server.Server, error) { - return server.NewServer(cfg, openfga, logger, tracer) +func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger, tracer tracing.Tracer, reg prometheus.Registerer) (*server.Server, error) { + return server.NewServer(cfg, openfga, logger, tracer, reg) } func NewHealthServer(target server.DiagnosticServer) *server.HealthServer { diff --git a/pkg/services/authz/zanzana/server/metrics.go b/pkg/services/authz/zanzana/server/metrics.go new file mode 100644 index 00000000000..7df91aed169 --- /dev/null +++ b/pkg/services/authz/zanzana/server/metrics.go @@ -0,0 +1,31 @@ +package server + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + metricsNamespace = "iam" + metricsSubSystem = "authz_zanzana_server" +) + +type metrics struct { + // requestDurationSeconds is a summary for zanzana server request duration + requestDurationSeconds *prometheus.HistogramVec +} + +func newZanzanaServerMetrics(reg prometheus.Registerer) *metrics { + return &metrics{ + requestDurationSeconds: promauto.With(reg).NewHistogramVec( + prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "Histogram for zanzana server request duration", + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Buckets: prometheus.ExponentialBuckets(0.00001, 4, 10), + }, + []string{"method", "namespace"}, + ), + } +} diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index f599fa6ae57..85635260c26 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/fullstorydev/grpchan/inprocgrpc" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/prometheus/client_golang/prometheus" "google.golang.org/protobuf/types/known/wrapperspb" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" @@ -43,8 +44,9 @@ type Server struct { storesMU *sync.Mutex cache *localcache.CacheService - logger log.Logger - tracer tracing.Tracer + logger log.Logger + tracer tracing.Tracer + metrics *metrics } type storeInfo struct { @@ -52,7 +54,7 @@ type storeInfo struct { ModelID string } -func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger log.Logger, tracer tracing.Tracer) (*Server, error) { +func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger log.Logger, tracer tracing.Tracer, reg prometheus.Registerer) (*Server, error) { channel := &inprocgrpc.Channel{} openfgav1.RegisterOpenFGAServiceServer(channel, openfga) openFGAClient := openfgav1.NewOpenFGAServiceClient(channel) @@ -66,6 +68,7 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger cache: localcache.New(cfg.CacheSettings.CheckQueryCacheTTL, cacheCleanInterval), logger: logger, tracer: tracer, + metrics: newZanzanaServerMetrics(reg), } return s, nil diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 0faadea7d0a..e9712b6fe21 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" @@ -16,9 +17,11 @@ import ( func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { ctx, span := s.tracer.Start(ctx, "server.Check") defer span.End() - span.SetAttributes( - attribute.String("namespace", r.GetNamespace()), - ) + span.SetAttributes(attribute.String("namespace", r.GetNamespace())) + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Check", r.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) res, err := s.check(ctx, r) if err != nil { diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index b8dfcfab4a5..a5fa19e3896 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -8,6 +8,7 @@ import ( "hash/fnv" "io" "strings" + "time" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" @@ -19,9 +20,11 @@ import ( func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.ListResponse, error) { ctx, span := s.tracer.Start(ctx, "server.List") defer span.End() - span.SetAttributes( - attribute.String("namespace", r.GetNamespace()), - ) + span.SetAttributes(attribute.String("namespace", r.GetNamespace())) + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.List", r.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) res, err := s.list(ctx, r) if err != nil { diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index da4141dce1c..d78d2fcbc0b 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" @@ -15,6 +16,10 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze ctx, span := s.tracer.Start(ctx, "server.Read") defer span.End() + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Read", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + res, err := s.read(ctx, req) if err != nil { s.logger.Error("failed to perform read request", "error", err, "namespace", req.GetNamespace()) diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 7b3c57155de..5d700c96a17 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -5,6 +5,7 @@ import ( "testing" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" authnlib "github.com/grafana/authlib/authn" @@ -92,7 +93,7 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, store) require.NoError(t, err) - srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService()) + srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry()) require.NoError(t, err) storeInf, err := srv.getStoreInfo(context.Background(), namespace) diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index 9ca10a38ec5..3cbbc6a5a32 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" @@ -15,6 +16,10 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth ctx, span := s.tracer.Start(ctx, "server.Write") defer span.End() + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Write", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + res, err := s.write(ctx, req) if err != nil { s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace()) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 009f7052d18..5b94492d18c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -315,13 +315,6 @@ var ( RequiresRestart: true, Owner: grafanaAppPlatformSquad, }, - { - Name: "provisioningSecretsService", - Description: "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", - Stage: FeatureStageExperimental, - RequiresRestart: true, - Owner: grafanaAppPlatformSquad, - }, { Name: "grafanaAPIServerEnsureKubectlAccess", Description: "Start an additional https handler and write kubectl options", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 88ad21535ac..46303a7f960 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -40,7 +40,6 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false 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 -provisioningSecretsService,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8f8db49668d..ee14a3e8f6c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -171,10 +171,6 @@ const ( // Next generation provisioning... and git FlagProvisioning = "provisioning" - // FlagProvisioningSecretsService - // Experimental feature to use the secrets service for provisioning instead of the legacy secrets - FlagProvisioningSecretsService = "provisioningSecretsService" - // FlagGrafanaAPIServerEnsureKubectlAccess // Start an additional https handler and write kubectl options FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index d117e50380a..ee2c04e2e22 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2640,7 +2640,8 @@ "metadata": { "name": "provisioningSecretsService", "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-25T13:06:00Z" + "creationTimestamp": "2025-07-25T13:06:00Z", + "deletionTimestamp": "2025-08-20T12:48:19Z" }, "spec": { "description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index d6fb86d31e2..72ea84396ae 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -461,7 +461,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger, workingFolderUID string) (*folder.FolderReference, response.Response) { logger.Debug("Getting or creating a new folder") - ns, err := srv.ruleStore.GetOrCreateNamespaceByTitle( + ns, created, err := srv.ruleStore.GetOrCreateNamespaceByTitle( c.Req.Context(), title, c.GetOrgID(), @@ -473,6 +473,26 @@ func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext return nil, namespaceErrorResponse(err) } + // Not all users have global-scoped permissions, even if they can create folders. + // For example, Editor users can create folders, but they have UID-scoped folder permissions. + // Permissions are populated in a middleware before this handler, and the folder we just created + // is not included in the permissions yet. We add it manually. + if created { + orgID := c.GetOrgID() + if c.Permissions == nil { + c.Permissions = make(map[int64]map[string][]string) + } + if c.Permissions[orgID] == nil { + c.Permissions[orgID] = make(map[string][]string) + } + + folderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(ns.UID) + if c.Permissions[orgID][dashboards.ActionFoldersRead] == nil { + c.Permissions[orgID][dashboards.ActionFoldersRead] = []string{} + } + c.Permissions[orgID][dashboards.ActionFoldersRead] = append(c.Permissions[orgID][dashboards.ActionFoldersRead], folderScope) + } + logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID) return ns, nil diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 926f3ebcc5b..d734bc698f5 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -511,6 +511,63 @@ func TestIntegrationProvisioningApi(t *testing.T) { require.Equal(t, 403, response.Status()) }) + + t.Run("with notification settings", func(t *testing.T) { + t.Run("POST returns 400 when receiver does not exist", func(t *testing.T) { + env := createTestEnv(t, testConfig) + env.nsValidator = &fakeRejectingNotificationSettingsValidatorProvider{} + sut := createProvisioningSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + rule := createTestAlertRule("rule", 1) + rule.NotificationSettings.Receiver = "non-existent-receiver" + + response := sut.RoutePostAlertRule(&rc, rule) + + require.Equal(t, 400, response.Status()) + require.Contains(t, string(response.Body()), "receiver non-existent-receiver does not exist") + }) + + t.Run("PUT returns 400 when receiver does not exist", func(t *testing.T) { + env := createTestEnv(t, testConfig) + sut := createProvisioningSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + rule := createTestAlertRule("rule", 1) + rule.UID = "test-uid" + insertRule(t, sut, rule) + + env.nsValidator = &fakeRejectingNotificationSettingsValidatorProvider{} + sut = createProvisioningSrvSutFromEnv(t, &env) + rule.NotificationSettings.Receiver = "non-existent-receiver" + + response := sut.RoutePutAlertRule(&rc, rule, rule.UID) + + require.Equal(t, 400, response.Status()) + require.Contains(t, string(response.Body()), "receiver non-existent-receiver does not exist") + }) + + t.Run("POST returns 201 when receiver exists", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + rule := createTestAlertRule("rule", 1) + + response := sut.RoutePostAlertRule(&rc, rule) + + require.Equal(t, 201, response.Status()) + }) + + t.Run("PUT returns 200 when receiver exists", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + rule := createTestAlertRule("rule", 1) + rule.UID = "test-uid-2" + insertRule(t, sut, rule) + rule.Title = "updated rule" + + response := sut.RoutePutAlertRule(&rc, rule, rule.UID) + + require.Equal(t, 200, response.Status()) + }) + }) }) t.Run("recording rules", func(t *testing.T) { @@ -1993,6 +2050,7 @@ type testEnvironment struct { user *user.SignedInUser rulesAuthz *fakes.FakeRuleService features featuremgmt.FeatureToggles + nsValidator provisioning.NotificationSettingsValidatorProvider } func createTestEnv(t *testing.T, testConfig string) testEnvironment { @@ -2111,6 +2169,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { user: user, rulesAuthz: ruleAuthz, features: features, + nsValidator: &provisioning.NotificationSettingsValidatorProviderFake{}, } } @@ -2142,7 +2201,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi contactPointService: provisioning.NewContactPointService(configStore, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store, ngalertfakes.NewFakeReceiverPermissionsService()), templates: provisioning.NewTemplateService(configStore, env.prov, env.xact, env.log), muteTimings: provisioning.NewMuteTimingService(configStore, env.prov, env.xact, env.log, env.store), - alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz), + alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, env.nsValidator, env.rulesAuthz), folderSvc: env.folderService, featureManager: env.features, } @@ -2254,6 +2313,12 @@ func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Conte type fakeRejectingNotificationPolicyService struct{} +type fakeRejectingNotificationSettingsValidatorProvider struct{} + +func (f *fakeRejectingNotificationSettingsValidatorProvider) Validator(ctx context.Context, orgID int64) (notifier.NotificationSettingsValidator, error) { + return notifier.RejectingValidation{}, nil +} + func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) { return definitions.Route{}, "", nil } diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index 5a60c2e0637..a3eb9af2b7c 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -16,7 +16,7 @@ type RuleStore interface { GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error) GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error) GetNamespaceByTitle(ctx context.Context, fullpath string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) - GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) + GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error) // GetNamespaceChildren returns all children (first level) of the namespace with the given id. GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.FolderReference, error) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 580338a4584..fe68bd47850 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -277,7 +277,7 @@ func (m MergeResult) LogContext() []any { return nil } logCtx := make([]any, 0, 4) - if len(m.RenamedTimeIntervals) > 0 { + if len(m.RenamedReceivers) > 0 { rcvBuilder := strings.Builder{} for from, to := range m.RenamedReceivers { rcvBuilder.WriteString(fmt.Sprintf("'%s'->'%s',", from, to)) @@ -285,11 +285,11 @@ func (m MergeResult) LogContext() []any { logCtx = append(logCtx, "renamedReceivers", fmt.Sprintf("[%s]", rcvBuilder.String()[0:rcvBuilder.Len()-1])) } if len(m.RenamedTimeIntervals) > 0 { - rcvBuilder := strings.Builder{} + intervalBuilder := strings.Builder{} for from, to := range m.RenamedTimeIntervals { - rcvBuilder.WriteString(fmt.Sprintf("'%s'->'%s',", from, to)) + intervalBuilder.WriteString(fmt.Sprintf("'%s'->'%s',", from, to)) } - logCtx = append(logCtx, "renamedTimeIntervals", fmt.Sprintf("[%s]", rcvBuilder.String()[0:rcvBuilder.Len()-1])) + logCtx = append(logCtx, "renamedTimeIntervals", fmt.Sprintf("[%s]", intervalBuilder.String()[0:intervalBuilder.Len()-1])) } return logCtx } diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index c26b8307a46..9fccf6d2f0d 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -235,6 +235,13 @@ func (n NoValidation) Validate(_ models.NotificationSettings) error { return nil } +type RejectingValidation struct { +} + +func (n RejectingValidation) Validate(s models.NotificationSettings) error { + return ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: s.Receiver}} +} + var errInvalidState = fmt.Errorf("invalid state") // silenceState copied from state in prometheus-alertmanager/silence/silence.go. diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 90662509d34..016fbc147f6 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -233,7 +233,7 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident } for _, setting := range rule.NotificationSettings { if err := validator.Validate(setting); err != nil { - return models.AlertRule{}, err + return models.AlertRule{}, errors.Join(models.ErrAlertRuleFailedValidation, err) } } } @@ -688,7 +688,7 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, user ident } for _, setting := range rule.NotificationSettings { if err := validator.Validate(setting); err != nil { - return models.AlertRule{}, err + return models.AlertRule{}, errors.Join(models.ErrAlertRuleFailedValidation, err) } } } diff --git a/pkg/services/ngalert/store/namespace.go b/pkg/services/ngalert/store/namespace.go index 2e28ee94ba6..645f72cbd70 100644 --- a/pkg/services/ngalert/store/namespace.go +++ b/pkg/services/ngalert/store/namespace.go @@ -95,24 +95,25 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, title string, orgID i // // To avoid race conditions when two concurrent requests try to create the same folder, // we create folders with a deterministic UID based on the parent UID, title, and organization ID. -func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) { +func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error) { if len(title) == 0 { - return nil, fmt.Errorf("title is empty") + return nil, false, fmt.Errorf("title is empty") } var f *folder.FolderReference var err error + var created bool f, err = st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID) if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) { - return nil, err + return nil, false, err } if f == nil { // Generate a deterministic UID with an alerting prefix uid, err := generateAlertingFolderUID(title, parentUID, orgID) if err != nil { - return nil, fmt.Errorf("error creating a new folder: %w", err) + return nil, false, fmt.Errorf("error creating a new folder: %w", err) } cmd := &folder.CreateFolderCommand{ @@ -129,20 +130,21 @@ func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, // the folder between our check and creation attempt existingFolder, lookupErr := st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID) if lookupErr == nil { - return existingFolder, nil + return existingFolder, false, nil } // If we couldn't find it, return errors - return nil, fmt.Errorf("failed to get or create folder: %w", errors.Join( + return nil, false, fmt.Errorf("failed to get or create folder: %w", errors.Join( fmt.Errorf("create folder: %w", err), fmt.Errorf("lookup folder: %w", lookupErr), )) } f = newFolder.ToFolderReference() + created = true } - return f, nil + return f, created, nil } // generateAlertingFolderUID creates a deterministic UID for folders diff --git a/pkg/services/ngalert/store/namespace_test.go b/pkg/services/ngalert/store/namespace_test.go index 03fea1b514e..fd17edec7b3 100644 --- a/pkg/services/ngalert/store/namespace_test.go +++ b/pkg/services/ngalert/store/namespace_test.go @@ -78,7 +78,8 @@ func TestGetNamespaceByTitle(t *testing.T) { func TestGetOrCreateNamespaceByTitle(t *testing.T) { store := DBstore{} - _, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID) + _, created, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID) + require.False(t, created) require.Error(t, err) require.Contains(t, err.Error(), "title is empty") diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index c17c70bf6c9..8e7d92666a5 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -363,13 +363,13 @@ func (f *RuleStore) GetNamespaceByUID(_ context.Context, uid string, orgID int64 return nil, dashboards.ErrFolderNotFound } -func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) { +func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error) { f.mtx.Lock() defer f.mtx.Unlock() for _, folder := range f.Folders[orgID] { if folder.Title == title && folder.ParentUID == parentUID { - return folder.ToFolderReference(), nil + return folder.ToFolderReference(), false, nil } } @@ -382,7 +382,7 @@ func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title strin } f.Folders[orgID] = append(f.Folders[orgID], newFolder) - return newFolder.ToFolderReference(), nil + return newFolder.ToFolderReference(), true, nil } func (f *RuleStore) GetNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) { diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 448d8ce2698..2f5582b2765 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -376,7 +376,7 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N } func (s *secureValueMetadataStorage) SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetVersionToActive", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), attribute.Int64("version", version), @@ -413,7 +413,7 @@ func (s *secureValueMetadataStorage) SetVersionToActive(ctx context.Context, nam } func (s *secureValueMetadataStorage) SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetVersionToInactive", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), attribute.Int64("version", version), diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 1b5ab0145dc..4d6a319c90d 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -1167,6 +1167,57 @@ func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) { }) } +func TestIntegrationConvertPrometheusEndpoints_Editor(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + testinfra.SQLiteIntegrationTest(t) + + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableRecordingRules: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + + adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Password: "password", + Login: "editor", + }) + editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "password") + + ds := adminClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + testGroup := apimodels.PrometheusRuleGroup{ + Name: "test-group-permission-cache", + Interval: prommodel.Duration(60 * time.Second), + Rules: []apimodels.PrometheusRule{ + { + Alert: "test-editor-permissions", + Expr: "vector(0)", + For: util.Pointer(prommodel.Duration(1 * time.Minute)), + }, + }, + } + + ns := util.GenerateShortUID() + + t.Run("editor can import rules that create new folder", func(t *testing.T) { + editorClient.ConvertPrometheusPostRuleGroup(t, ns, ds.Body.Datasource.UID, testGroup, nil) + + group := editorClient.ConvertPrometheusGetRuleGroupRules(t, ns, testGroup.Name, nil) + require.Equal(t, testGroup.Name, group.Name) + require.Len(t, group.Rules, 1) + require.Equal(t, testGroup.Rules[0].Alert, group.Rules[0].Alert) + }) +} + func TestIntegrationConvertPrometheusEndpoints_GroupLabels(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") 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 39bf07a014c..a7989e33e4e 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2605,20 +2605,10 @@ "type": "string", "default": "" }, - "encryptedToken": { - "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - "type": "string", - "format": "byte", - "x-kubernetes-list-type": "atomic" - }, "path": { "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", "type": "string" }, - "token": { - "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - "type": "string" - }, "tokenUser": { "description": "TokenUser is the user that will be used to access the repository if it's a personal access token.", "type": "string" @@ -2761,12 +2751,6 @@ "type": "string", "default": "" }, - "encryptedToken": { - "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - "type": "string", - "format": "byte", - "x-kubernetes-list-type": "atomic" - }, "generateDashboardPreviews": { "description": "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).", "type": "boolean" @@ -2775,10 +2759,6 @@ "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", "type": "string" }, - "token": { - "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - "type": "string" - }, "url": { "description": "The repository URL (e.g. `https://github.com/example/test`).", "type": "string" @@ -2796,20 +2776,10 @@ "type": "string", "default": "" }, - "encryptedToken": { - "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - "type": "string", - "format": "byte", - "x-kubernetes-list-type": "atomic" - }, "path": { "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", "type": "string" }, - "token": { - "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - "type": "string" - }, "url": { "description": "The repository URL (e.g. `https://gitlab.com/example/test`).", "type": "string" @@ -2827,20 +2797,10 @@ "type": "string", "default": "" }, - "encryptedToken": { - "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", - "type": "string", - "format": "byte", - "x-kubernetes-list-type": "atomic" - }, "path": { "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", "type": "string" }, - "token": { - "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", - "type": "string" - }, "tokenUser": { "description": "TokenUser is the user that will be used to access the repository if it's a personal access token.", "type": "string" @@ -4213,7 +4173,6 @@ ] }, "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SecureValues": { - "description": "NOT YET USED FOR REAL -- testing secure value workflow", "type": "object", "properties": { "token": { @@ -4226,7 +4185,7 @@ ] }, "webhookSecret": { - "description": "Some webhooks (github) require a secret key value", + "description": "Some webhooks (including github) require a secret key value", "default": {}, "allOf": [ { @@ -4422,10 +4381,6 @@ "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.WebhookStatus": { "type": "object", "properties": { - "encryptedSecret": { - "type": "string", - "format": "byte" - }, "id": { "type": "integer", "format": "int64" @@ -4434,9 +4389,6 @@ "type": "integer", "format": "int64" }, - "secret": { - "type": "string" - }, "subscribedEvents": { "type": "array", "items": { diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index 0fb50148e8b..1d03cbe1d02 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -570,17 +570,9 @@ func withLogs(opts *testinfra.GrafanaOpts) { opts.EnableLog = true } -func useAppPlatformSecrets(opts *testinfra.GrafanaOpts) { - opts.EnableFeatureToggles = append(opts.EnableFeatureToggles, - featuremgmt.FlagProvisioningSecretsService, - featuremgmt.FlagSecretsManagementAppPlatform, - ) -} - func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper { provisioningPath := t.TempDir() opts := testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs EnableFeatureToggles: []string{ featuremgmt.FlagProvisioning, }, diff --git a/pkg/tests/apis/provisioning/repository_test.go b/pkg/tests/apis/provisioning/repository_test.go index ad6a254059e..7f69d33511c 100644 --- a/pkg/tests/apis/provisioning/repository_test.go +++ b/pkg/tests/apis/provisioning/repository_test.go @@ -49,12 +49,12 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { require.NoError(t, err, "failed to read back resource") // Move encrypted token mutation - token, found, err := unstructured.NestedString(output.Object, "spec", "github", "encryptedToken") - require.NoError(t, err, "encryptedToken is not a string") + token, found, err := unstructured.NestedString(output.Object, "secure", "token", "name") + require.NoError(t, err, "secure token name 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") + require.True(t, strings.HasPrefix("inline-", token)) // name created automatically + err = unstructured.SetNestedField(input.Object, token, "secure", "token", "name") + require.NoError(t, err, "unable to copy secure token") } // Marshal as real objects to ",omitempty" values are tested properly diff --git a/pkg/tests/apis/provisioning/secrets_test.go b/pkg/tests/apis/provisioning/secrets_test.go index ba571fe25bb..fcdabe61197 100644 --- a/pkg/tests/apis/provisioning/secrets_test.go +++ b/pkg/tests/apis/provisioning/secrets_test.go @@ -2,9 +2,6 @@ package provisioning import ( "context" - "encoding/base64" - "errors" - "fmt" "strings" "testing" "time" @@ -13,10 +10,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" ) func TestIntegrationProvisioning_InlineSecrets(t *testing.T) { @@ -24,12 +17,12 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) { t.Skip("skipping integration test") } - helper := runGrafana(t, useAppPlatformSecrets) + helper := runGrafana(t) createOptions := metav1.CreateOptions{FieldValidation: "Strict"} ctx := context.Background() decryptService := helper.GetEnv().DecryptService - require.NotNil(t, decryptService, "decrypt service wired properly") + require.NotNil(t, decryptService, "decrypt service not wired properly") type expectedField struct { Path []string @@ -107,519 +100,3 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) { }) } } - -func TestIntegrationProvisioning_LegacySecrets(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - helper := runGrafana(t) - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} - ctx := context.Background() - - type expectedField struct { - Path []string - ExpectedDecryptedValue string - } - - secretsService := helper.GetEnv().RepositorySecrets - tests := []struct { - name string - values map[string]any - inputFile string - expectedFields []expectedField - }{ - { - name: "github token encrypted", - values: map[string]any{ - "Token": "some-token", - }, - inputFile: "testdata/github-readonly.json.tmpl", - expectedFields: []expectedField{ - { - Path: []string{"spec", "github", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "github", "encryptedToken"}, - ExpectedDecryptedValue: "some-token", - }, - }, - }, - { - name: "git token encrypted", - values: map[string]any{ - "Token": "some-token", - }, - inputFile: "testdata/git-readonly.json.tmpl", - expectedFields: []expectedField{ - { - Path: []string{"spec", "git", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "git", "encryptedToken"}, - ExpectedDecryptedValue: "some-token", - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - input := helper.RenderObject(t, test.inputFile, test.values) - _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") - - 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") - repo := unstructuredToRepository(t, output) - - // Move encrypted token mutation - for _, expectedField := range test.expectedFields { - value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") - require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") - require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) - } - }) - } -} - -func TestIntegrationProvisioning_Secrets_LegacyUpdate(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - helper := runGrafana(t) - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} - updateOptions := metav1.UpdateOptions{} - ctx := context.Background() - - secretsService := helper.GetEnv().RepositorySecrets - - type expectedField struct { - Path []string - ExpectedValue string - ExpectedDecryptedValue string - } - - tests := []struct { - name string - values map[string]any - inputFile string - updateValues map[string]any - expectedFields []expectedField - }{ - { - name: "update github token (legacy secrets)", - values: map[string]any{ - "Token": "initial-token", - }, - inputFile: "testdata/github-readonly.json.tmpl", - updateValues: map[string]any{ - "Token": "updated-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "github", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "github", "encryptedToken"}, - ExpectedDecryptedValue: "updated-token", - }, - }, - }, - { - name: "update git token (legacy secrets)", - values: map[string]any{ - "Token": "initial-token", - }, - inputFile: "testdata/git-readonly.json.tmpl", - updateValues: map[string]any{ - "Token": "updated-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "git", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "git", "encryptedToken"}, - ExpectedDecryptedValue: "updated-token", - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // Create initial resource - input := helper.RenderObject(t, test.inputFile, test.values) - _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") - - name := mustNestedString(input.Object, "metadata", "name") - - // Prepare updated resource - updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) - // Set the same name and resourceVersion for update - updatedInput.Object["metadata"].(map[string]any)["name"] = name - - // Fetch current resourceVersion - current, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) - require.NoError(t, err, "failed to get current resource for update") - updatedInput.Object["metadata"].(map[string]any)["resourceVersion"] = current.Object["metadata"].(map[string]any)["resourceVersion"] - - _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) - require.NoError(t, err, "failed to update resource") - - output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) - require.NoError(t, err, "failed to read back resource after update") - repo := unstructuredToRepository(t, output) - - for _, expectedField := range test.expectedFields { - value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") - require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") - require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) - } - }) - } -} - -func TestIntegrationProvisioning_Secrets(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - helper := runGrafana(t, useAppPlatformSecrets) - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} - ctx := context.Background() - - secretsService := helper.GetEnv().RepositorySecrets - - type expectedField struct { - Path []string - ExpectedValue string - ExpectedDecryptedValue string - } - // TODO: Add test of fallbacks - tests := []struct { - name string - values map[string]any - inputFile string - expectedFields []expectedField - }{ - { - name: "github token encrypted", - values: map[string]any{ - "Token": "some-token", - }, - inputFile: "testdata/github-readonly.json.tmpl", - expectedFields: []expectedField{ - { - Path: []string{"spec", "github", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "github", "encryptedToken"}, - ExpectedValue: "github-token", - ExpectedDecryptedValue: "some-token", - }, - }, - }, - { - name: "git token encrypted", - values: map[string]any{ - "Token": "some-token", - }, - inputFile: "testdata/git-readonly.json.tmpl", - expectedFields: []expectedField{ - { - Path: []string{"spec", "git", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "git", "encryptedToken"}, - ExpectedValue: "git-token", - ExpectedDecryptedValue: "some-token", - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - input := helper.RenderObject(t, test.inputFile, test.values) - _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") - - 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") - repo := unstructuredToRepository(t, output) - - // Move encrypted token mutation - for _, expectedField := range test.expectedFields { - value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") - - if expectedField.ExpectedValue != "" { - require.Equal(t, name+"-"+expectedField.ExpectedValue, value) - } - - if expectedField.ExpectedDecryptedValue != "" { - require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) - } - } - }) - } -} - -func TestIntegrationProvisioning_Secrets_Update(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } - ctx := context.Background() - helper := runGrafana(t, useAppPlatformSecrets) - secretsService := helper.GetEnv().RepositorySecrets - createOptions := metav1.CreateOptions{} - updateOptions := metav1.UpdateOptions{} - - type expectedField struct { - Path []string - ExpectedValue string - ExpectedDecryptedValue string - } - - tests := []struct { - name string - inputFile string - values map[string]interface{} - updateValues map[string]interface{} - expectedFields []expectedField - updatedFields []expectedField - }{ - { - name: "update encrypted git token", - inputFile: "testdata/git-readonly.json.tmpl", - values: map[string]interface{}{ - "Token": "initial-token", - }, - updateValues: map[string]interface{}{ - "Token": "updated-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "git", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "git", "encryptedToken"}, - ExpectedValue: "git-token", - ExpectedDecryptedValue: "initial-token", - }, - }, - updatedFields: []expectedField{ - { - Path: []string{"spec", "git", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "git", "encryptedToken"}, - ExpectedValue: "git-token", - ExpectedDecryptedValue: "updated-token", - }, - }, - }, - { - name: "update encrypted github token", - inputFile: "testdata/github-readonly.json.tmpl", - values: map[string]interface{}{ - "Token": "initial-token", - }, - updateValues: map[string]interface{}{ - "Token": "updated-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "github", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "github", "encryptedToken"}, - ExpectedValue: "github-token", - ExpectedDecryptedValue: "initial-token", - }, - }, - updatedFields: []expectedField{ - { - Path: []string{"spec", "github", "token"}, - ExpectedDecryptedValue: "", - }, - { - Path: []string{"spec", "github", "encryptedToken"}, - ExpectedValue: "github-token", - ExpectedDecryptedValue: "updated-token", - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // Create initial resource - input := helper.RenderObject(t, test.inputFile, test.values) - _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") - - 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") - - // Update the resource - updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) - // Set the same name and resourceVersion for update - _ = unstructured.SetNestedField(updatedInput.Object, name, "metadata", "name") - _ = unstructured.SetNestedField(updatedInput.Object, output.GetResourceVersion(), "metadata", "resourceVersion") - _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) - require.NoError(t, err, "failed to update resource") - - updatedOutput, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) - require.NoError(t, err, "failed to read back updated resource") - updatedRepo := unstructuredToRepository(t, updatedOutput) - - // Check updated fields - for _, expectedField := range test.updatedFields { - value, decrypted := encryptedField(t, secretsService, updatedRepo, updatedOutput.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") - - if expectedField.ExpectedValue != "" { - require.Equal(t, name+"-"+expectedField.ExpectedValue, value) - } - - if expectedField.ExpectedDecryptedValue != "" { - require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) - } - } - }) - } -} - -func TestIntegrationProvisioning_Secrets_Removal(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - helper := runGrafana(t, useAppPlatformSecrets) - secretsService := helper.GetEnv().RepositorySecrets - createOptions := metav1.CreateOptions{} - - type expectedField struct { - Path []string - } - - tests := []struct { - name string - inputFile string - values map[string]interface{} - expectedFields []expectedField - updatedFields []expectedField - }{ - { - name: "remove encrypted git token", - inputFile: "testdata/git-readonly.json.tmpl", - values: map[string]interface{}{ - "Token": "initial-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "git", "encryptedToken"}, - }, - }, - }, - { - name: "remove encrypted github token", - inputFile: "testdata/github-readonly.json.tmpl", - values: map[string]interface{}{ - "Token": "initial-token", - }, - expectedFields: []expectedField{ - { - Path: []string{"spec", "github", "encryptedToken"}, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // Create initial resource - input := helper.RenderObject(t, test.inputFile, test.values) - _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) - require.NoError(t, err, "failed to create resource") - - 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") - - repo := unstructuredToRepository(t, output) - - // Set the same name and resourceVersion for update - err = helper.Repositories.Resource.Delete(ctx, name, metav1.DeleteOptions{}) - require.NoError(t, err, "failed to delete resource") - - for _, expectedField := range test.expectedFields { - secretName, found, err := base64DecodedField(output.Object, expectedField.Path) - require.NoError(t, err, "failed to decode base64 value") - require.True(t, found, "secretName should be found") - require.NotEmpty(t, secretName) - - var lastDecrypted []byte - require.Eventually(t, func() bool { - lastDecrypted, err = secretsService.Decrypt(ctx, repo, secretName) - return err != nil && errors.Is(err, contracts.ErrDecryptNotFound) - }, 1000*time.Second, 500*time.Millisecond, "expected ErrDecryptNotFound error, got %v", lastDecrypted) - } - }) - } -} - -func encryptedField(t *testing.T, secretsService secrets.RepositorySecrets, repo *provisioning.Repository, obj map[string]any, path []string, expectedValue bool) (string, string) { - value, found, err := base64DecodedField(obj, path) - if err != nil { - require.NoError(t, err, "failed to decode base64 value") - } - - if expectedValue { - decrypted, err := secretsService.Decrypt(context.Background(), repo, value) - require.NoError(t, err, "failed to eecrypt value") - return value, string(decrypted) - } else { - require.False(t, found, "value should not be found") - return "", "" - } -} - -func base64DecodedField(obj map[string]any, path []string) (string, bool, error) { - value, found, err := unstructured.NestedFieldNoCopy(obj, path...) - if err != nil { - return "", false, err - } - - if !found { - return "", false, nil - } - - valueStr, ok := value.(string) - if !ok { - return "", false, fmt.Errorf("value is not a string") - } - - decodedValue, err := base64.StdEncoding.DecodeString(valueStr) - if err != nil { - return "", false, fmt.Errorf("failed to decode base64 valueStr: %w", err) - } - - return string(decodedValue), true, nil -} diff --git a/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl index 38f0201b49f..671417fa35b 100644 --- a/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl +++ b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl @@ -11,7 +11,6 @@ "git": { "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", "branch": "{{ or .Branch "integration-test" }}", - "token": "{{ or .Token "" }}", "path": "{{ or .Path "grafana/" }}" }, "sync": { @@ -20,5 +19,8 @@ "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} }, "workflows": [] + }, + "secure": { + "token": { "create": "{{ or .Token "" }}" } } } diff --git a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl index 86489c7d65c..84549924de3 100644 --- a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl +++ b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl @@ -12,7 +12,6 @@ "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", "branch": "{{ or .Branch "integration-test" }}", "generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }}, - "token": "{{ or .Token "" }}", "path": "{{ or .Path "grafana/" }}" }, "sync": { @@ -21,5 +20,8 @@ "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} }, "workflows": [] + }, + "secure": { + "token": { "create": "{{ or .Token "" }}" } } } diff --git a/pkg/tests/apis/provisioning/testdata/github-with-inline-secrets.json.tmpl b/pkg/tests/apis/provisioning/testdata/github-with-inline-secrets.json.tmpl index c867b6472d0..7c03ed2089b 100644 --- a/pkg/tests/apis/provisioning/testdata/github-with-inline-secrets.json.tmpl +++ b/pkg/tests/apis/provisioning/testdata/github-with-inline-secrets.json.tmpl @@ -12,7 +12,6 @@ "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", "branch": "{{ or .Branch "integration-test" }}", "generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }}, - "token": "{{ or .Token "" }}", "path": "{{ or .Path "grafana/" }}" } }, diff --git a/public/app/api/clients/folder/v1beta1/hooks.test.ts b/public/app/api/clients/folder/v1beta1/hooks.test.ts index 5d7de2639f2..5c3936a4847 100644 --- a/public/app/api/clients/folder/v1beta1/hooks.test.ts +++ b/public/app/api/clients/folder/v1beta1/hooks.test.ts @@ -1,175 +1,107 @@ -import { QueryStatus } from '@reduxjs/toolkit/query'; -import { renderHook } from '@testing-library/react'; +import { renderHook, getWrapper, waitFor } from 'test/test-utils'; -import { config } from '@grafana/runtime'; -import { useGetFolderQuery as useGetFolderQueryLegacy } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; - -import { - AnnoKeyCreatedBy, - AnnoKeyFolder, - AnnoKeyManagerKind, - AnnoKeyUpdatedBy, - AnnoKeyUpdatedTimestamp, - DeprecatedInternalId, -} from '../../../../features/apiserver/types'; -import { useGetDisplayMappingQuery } from '../../iam/v0alpha1'; +import { config, setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; import { useGetFolderQueryFacade } from './hooks'; -import { useGetFolderQuery, useGetFolderParentsQuery } from './index'; +setBackendSrv(backendSrv); +setupMockServer(); -// Mocks for the hooks used inside useGetFolderQueryFacade -jest.mock('./index', () => ({ - useGetFolderQuery: jest.fn(), - useGetFolderParentsQuery: jest.fn(), -})); +const [_, { folderA, folderA_folderA }] = getFolderFixtures(); -jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({ - useGetFolderQuery: jest.fn(), -})); +const expectedUid = folderA_folderA.item.uid; +const expectedTitle = folderA_folderA.item.title; +const urlSlug = expectedTitle.toLowerCase().replace(/ /g, '-').replace(/[\.]/g, ''); +const expectedUrl = `/grafana/dashboards/f/${expectedUid}/${urlSlug}`; -jest.mock('../../iam/v0alpha1', () => ({ - useGetDisplayMappingQuery: jest.fn(), -})); +const parentUrlSlug = folderA.item.title.toLowerCase().replace(/ /g, '-').replace(/[\.]/g, ''); +const expectedParentUrl = `/grafana/dashboards/f/${folderA.item.uid}/${parentUrlSlug}`; -// Mock config and constants -jest.mock('@grafana/runtime', () => { - const runtime = jest.requireActual('@grafana/runtime'); - return { - ...runtime, - config: { - ...runtime.config, - featureToggles: { - ...runtime.config.featureToggles, - foldersAppPlatformAPI: true, - }, - appSubUrl: '/grafana', - }, - }; -}); - -const mockFolder = { - data: { - metadata: { - name: 'folder-uid', - labels: { [DeprecatedInternalId]: '123' }, - annotations: { - [AnnoKeyUpdatedBy]: 'user-1', - [AnnoKeyCreatedBy]: 'user-2', - [AnnoKeyFolder]: 'parent-uid', - [AnnoKeyManagerKind]: 'user', - [AnnoKeyUpdatedTimestamp]: '2024-01-01T00:00:00Z', - }, - creationTimestamp: '2023-01-01T00:00:00Z', - generation: 2, - }, - spec: { title: 'Test Folder' }, - }, - ...getResponseAttributes(), +const renderFolderHook = async () => { + const { result } = renderHook(() => useGetFolderQueryFacade(folderA_folderA.item.uid), { + wrapper: getWrapper({}), + }); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + return result; }; -const mockParents = { - data: { items: [{ name: 'parent-uid', title: 'Parent Folder' }] }, - ...getResponseAttributes(), -}; - -const mockLegacyResponse = { - data: { - id: 1, - uid: 'uiduiduid', - orgId: 1, - title: 'bar', - url: '/dashboards/f/uiduiduid/bar', - hasAcl: false, - canSave: true, - canEdit: true, - canAdmin: true, - canDelete: true, - createdBy: 'Anonymous', - created: '2025-07-14T12:07:36+02:00', - updatedBy: 'Anonymous', - updated: '2025-07-15T18:01:36+02:00', - version: 1, - accessControl: { - 'dashboards.permissions:write': true, - 'dashboards:create': true, - }, - }, - ...getResponseAttributes(), -}; - -const mockUserDisplay = { - data: { - keys: ['user-1', 'user-2'], - display: [{ displayName: 'User One' }, { displayName: 'User Two' }], - }, - ...getResponseAttributes(), -}; +const originalToggles = { ...config.featureToggles }; +const originalAppSubUrl = String(config.appSubUrl); describe('useGetFolderQueryFacade', () => { - const oldToggleValue = config.featureToggles.foldersAppPlatformAPI; - - afterAll(() => { - config.featureToggles.foldersAppPlatformAPI = oldToggleValue; - }); - beforeEach(() => { - (useGetFolderQuery as jest.Mock).mockReturnValue(mockFolder); - (useGetFolderParentsQuery as jest.Mock).mockReturnValue(mockParents); - (useGetDisplayMappingQuery as jest.Mock).mockReturnValue(mockUserDisplay); - (useGetFolderQueryLegacy as jest.Mock).mockReturnValue(mockLegacyResponse); + config.appSubUrl = '/grafana'; }); - it('merges multiple responses into a single FolderDTO-like object if flag is true', () => { + afterEach(() => { + config.featureToggles = originalToggles; + config.appSubUrl = originalAppSubUrl; + }); + + it('merges multiple responses into a single FolderDTO-like object if flag is true', async () => { config.featureToggles.foldersAppPlatformAPI = true; - const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid')); + + const result = await renderFolderHook(); + expect(result.current.data).toMatchObject({ canAdmin: true, canDelete: true, canEdit: true, canSave: true, created: '2023-01-01T00:00:00Z', - createdBy: 'User Two', + createdBy: 'User 1', hasAcl: false, id: 123, - parentUid: 'parent-uid', + parentUid: folderA.item.uid, managedBy: 'user', - title: 'Test Folder', - uid: 'folder-uid', + title: expectedTitle, + uid: expectedUid, updated: '2024-01-01T00:00:00Z', - updatedBy: 'User One', - url: '/grafana/dashboards/f/folder-uid/test-folder', - version: 2, + updatedBy: 'User 2', + url: expectedUrl, + version: 1, accessControl: { 'dashboards.permissions:write': true, 'dashboards:create': true, }, parents: [ { - title: 'Parent Folder', - uid: 'parent-uid', - url: '/grafana/dashboards/f/parent-uid/parent-folder', + title: folderA.item.title, + uid: folderA.item.uid, + url: expectedParentUrl, }, ], }); }); - it('returns legacy folder response if flag is false', () => { + it('returns legacy folder response if flag is false', async () => { config.featureToggles.foldersAppPlatformAPI = false; - const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid')); - expect(result.current.data).toMatchObject(mockLegacyResponse.data); + const result = await renderFolderHook(); + expect(result.current.data).toMatchObject({ + id: 791, + title: folderA_folderA.item.title, + url: expectedUrl, + uid: expectedUid, + orgId: 1, + hasAcl: false, + canSave: true, + canEdit: true, + canAdmin: true, + canDelete: true, + createdBy: 'Anonymous', + created: '2025-07-14T12:07:36+02:00', + updatedBy: 'Anonymous', + updated: '2025-07-15T18:01:36+02:00', + version: 1, + accessControl: { + 'dashboards.permissions:write': true, + 'dashboards:create': true, + }, + }); }); }); - -function getResponseAttributes() { - return { - status: QueryStatus.fulfilled, - isUninitialized: false, - isLoading: false, - isFetching: false, - isSuccess: true, - isError: false, - error: undefined, - refetch: jest.fn(), - }; -} diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index b20424ce1ad..da5c0bfb9b3 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -938,20 +938,16 @@ export type InlineSecureValue = export type SecureValues = { /** Token used to connect the configured repository */ token?: InlineSecureValue; - /** Some webhooks (github) require a secret key value */ + /** Some webhooks (including github) require a secret key value */ webhookSecret?: InlineSecureValue; }; export type BitbucketRepositoryConfig = { /** The branch to use in the repository. */ branch: string; - /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ - encryptedToken?: string; /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ path?: string; - /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ - token?: string; /** TokenUser is the user that will be used to access the repository if it's a personal access token. */ tokenUser?: string; /** The repository URL (e.g. `https://bitbucket.org/example/test`). */ @@ -960,14 +956,10 @@ export type BitbucketRepositoryConfig = { export type GitRepositoryConfig = { /** The branch to use in the repository. */ branch: string; - /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ - encryptedToken?: string; /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ path?: string; - /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ - token?: string; /** TokenUser is the user that will be used to access the repository if it's a personal access token. */ tokenUser?: string; /** The repository URL (e.g. `https://github.com/example/test.git`). */ @@ -976,30 +968,22 @@ export type GitRepositoryConfig = { export type GitHubRepositoryConfig = { /** The branch to use in the repository. */ branch: string; - /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ - encryptedToken?: string; /** Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews). */ generateDashboardPreviews?: boolean; /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ path?: string; - /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ - token?: string; /** The repository URL (e.g. `https://github.com/example/test`). */ url?: string; }; export type GitLabRepositoryConfig = { /** The branch to use in the repository. */ branch: string; - /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ - encryptedToken?: string; /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ path?: string; - /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ - token?: string; /** The repository URL (e.g. `https://gitlab.com/example/test`). */ url?: string; }; @@ -1092,10 +1076,8 @@ export type SyncStatus = { state: 'error' | 'pending' | 'success' | 'warning' | 'working'; }; export type WebhookStatus = { - encryptedSecret?: string; id?: number; lastEvent?: number; - secret?: string; subscribedEvents?: string[]; url?: string; }; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 27b34e2f41e..fc8bd0bf00c 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -36,6 +36,9 @@ export interface NestedFolderPickerProps { /* Folder UIDs to exclude from the picker, to prevent invalid operations */ excludeUIDs?: string[]; + /* Start tree from this folder instead of root */ + rootFolderUID?: string; + /* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */ permission?: 'view' | 'edit'; @@ -44,9 +47,6 @@ export interface NestedFolderPickerProps { /* Whether the picker should be clearable */ clearable?: boolean; - - /* HTML ID for the button element for form labels */ - id?: string; } const debouncedSearch = debounce(getSearchResults, 300); @@ -69,9 +69,9 @@ export function NestedFolderPicker({ showRootFolder = true, clearable = false, excludeUIDs, + rootFolderUID, permission = 'edit', onChange, - id, }: NestedFolderPickerProps) { const styles = useStyles2(getStyles); const selectedFolder = useGetFolderQueryFacade(value); @@ -106,7 +106,7 @@ export function NestedFolderPicker({ items: browseFlatTree, isLoading: isBrowseLoading, requestNextPage: fetchFolderPage, - } = useFoldersQuery(isBrowsing, foldersOpenState, permissionLevel); + } = useFoldersQuery(isBrowsing, foldersOpenState, permissionLevel, rootFolderUID); useEffect(() => { if (!search) { @@ -287,7 +287,6 @@ export function NestedFolderPicker({ if (!overlayOpen) { return ( , - permission?: PermissionLevelString + permission?: PermissionLevelString, + /* Start tree from this folder instead of root */ + rootFolderUID?: string ) { - const resultLegacy = useFoldersQueryLegacy(isBrowsing, openFolders, permission); - const resultAppPlatform = useFoldersQueryAppPlatform(isBrowsing, openFolders); + const resultLegacy = useFoldersQueryLegacy(isBrowsing, openFolders, permission, rootFolderUID); + const resultAppPlatform = useFoldersQueryAppPlatform(isBrowsing, openFolders, rootFolderUID); // Running the hooks themselves don't have any side effects, so we can just conditionally use one or the other // requestNextPage function from the result diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts index b877c69aba0..b1541cbfcd2 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts @@ -25,7 +25,12 @@ const collator = new Intl.Collator(); * This version uses the getFolderChildren API from the folder v1beta1 API. Compared to legacy API, the v1beta1 API * does not have pagination at the moment. */ -export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Record) { +export function useFoldersQueryAppPlatform( + isBrowsing: boolean, + openFolders: Record, + /* rootFolderUID: configure which folder to start browsing from */ + rootFolderUID?: string +) { const dispatch = useDispatch(); // Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted @@ -150,11 +155,12 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec return list; } - const rootFlatTree = createFlatList(rootFolderToken, state.responseByParent[rootFolderToken], 1); + const startingToken = rootFolderUID ?? rootFolderToken; + const rootFlatTree = createFlatList(startingToken, state.responseByParent[startingToken], 1); rootFlatTree.unshift(getRootFolderItem()); return rootFlatTree; - }, [state, isBrowsing, openFolders]); + }, [state, isBrowsing, openFolders, rootFolderUID]); return { items: treeList, diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts index 84c540f7f71..52e33a538d7 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts @@ -48,7 +48,9 @@ function getPagesLoadStatus(pages: ListFoldersQuery[]): [boolean, number | undef export function useFoldersQueryLegacy( isBrowsing: boolean, openFolders: Record, - permission?: PermissionLevelString + permission?: PermissionLevelString, + /* rootFolderUID: configure which folder to start browsing from */ + rootFolderUID?: string ) { const dispatch = useDispatch(); @@ -178,11 +180,13 @@ export function useFoldersQueryLegacy( return flatList; } - const rootFlatTree = createFlatList(undefined, state.rootPages, 1); + const startingPages = rootFolderUID ? state.pagesByParent[rootFolderUID] : state.rootPages; + + const rootFlatTree = createFlatList(rootFolderUID ?? undefined, startingPages ?? [], 1); rootFlatTree.unshift(getRootFolderItem()); return rootFlatTree; - }, [state, isBrowsing, openFolders]); + }, [state, isBrowsing, openFolders, rootFolderUID]); return { items: treeList, diff --git a/public/app/core/components/OptionsUI/NumberInput.tsx b/public/app/core/components/OptionsUI/NumberInput.tsx index b96893096d8..f5ba98492b7 100644 --- a/public/app/core/components/OptionsUI/NumberInput.tsx +++ b/public/app/core/components/OptionsUI/NumberInput.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { Field, Input } from '@grafana/ui'; interface Props { - id?: string; value?: number; placeholder?: string; autoFocus?: boolean; @@ -102,7 +101,6 @@ export class NumberInput extends PureComponent { return ( void; settings?: ColorValueEditorSettings; @@ -26,7 +25,7 @@ interface Props { /** * @alpha * */ -export const ColorValueEditor = ({ value, settings, onChange, details, id }: Props) => { +export const ColorValueEditor = ({ value, settings, onChange, details }: Props) => { const theme = useTheme2(); const styles = useStyles2(getStyles); @@ -38,7 +37,6 @@ export const ColorValueEditor = ({ value, settings, onChange, details, id }: Pro
extends PureComponent, State> render() { const { options, isLoading } = this.state; - const { value, onChange, item, id } = this.props; + const { value, onChange, item } = this.props; const { settings } = item; return ( - inputId={id} isLoading={isLoading} value={value} defaultValue={value} diff --git a/public/app/core/components/OptionsUI/number.tsx b/public/app/core/components/OptionsUI/number.tsx index cd69f2b0370..e7b0db58bbe 100644 --- a/public/app/core/components/OptionsUI/number.tsx +++ b/public/app/core/components/OptionsUI/number.tsx @@ -6,7 +6,7 @@ import { NumberInput } from './NumberInput'; type Props = StandardEditorProps; -export const NumberValueEditor = ({ value, onChange, item, id }: Props) => { +export const NumberValueEditor = ({ value, onChange, item }: Props) => { const { settings } = item; const onValueChange = useCallback( @@ -18,7 +18,6 @@ export const NumberValueEditor = ({ value, onChange, item, id }: Props) => { return ( extends PureComponent, State> { render() { const { options, isLoading } = this.state; - const { value, onChange, item, id } = this.props; + const { value, onChange, item } = this.props; const { settings } = item; let current = options.find((v) => v.value === value); @@ -63,7 +63,6 @@ export class SelectValueEditor extends PureComponent, State> { } return ( - inputId={id} isLoading={isLoading} value={current} defaultValue={value} diff --git a/public/app/core/components/OptionsUI/slider.tsx b/public/app/core/components/OptionsUI/slider.tsx index 01835ea9a38..b9bb41bbd90 100644 --- a/public/app/core/components/OptionsUI/slider.tsx +++ b/public/app/core/components/OptionsUI/slider.tsx @@ -11,7 +11,7 @@ import { NumberInput } from './NumberInput'; type Props = StandardEditorProps; -export const SliderValueEditor = ({ value, onChange, item, id }: Props) => { +export const SliderValueEditor = ({ value, onChange, item }: Props) => { // Input reference const inputRef = useRef(null); @@ -109,7 +109,7 @@ export const SliderValueEditor = ({ value, onChange, item, id }: Props) => { included={included} /> - +
diff --git a/public/app/core/components/OptionsUI/string.tsx b/public/app/core/components/OptionsUI/string.tsx index 7ec4635f7c8..8001bf096af 100644 --- a/public/app/core/components/OptionsUI/string.tsx +++ b/public/app/core/components/OptionsUI/string.tsx @@ -8,7 +8,7 @@ interface Props extends StandardEditorProps { suffix?: ReactNode; } -export const StringValueEditor = ({ value, onChange, item, suffix, id }: Props) => { +export const StringValueEditor = ({ value, onChange, item, suffix }: Props) => { const Component = item.settings?.useTextarea ? TextArea : Input; const onValueChange = useCallback( ( @@ -36,7 +36,6 @@ export const StringValueEditor = ({ value, onChange, item, suffix, id }: Props) return ( ; -export function UnitValueEditor({ value, onChange, item, id }: Props) { +export function UnitValueEditor({ value, onChange, item }: Props) { const styles = useStyles2(getStyles); if (item?.settings?.isClearable && value != null) { return (
- + ); } - return ; + return ; } const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 2f139066a5b..ec91b28d348 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -1,4 +1,4 @@ -import { isEmpty, pickBy } from 'lodash'; +import { pickBy } from 'lodash'; import { config, createMonitoringLogger, reportInteraction } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; @@ -7,9 +7,9 @@ import { RuleNamespace } from '../../../types/unified-alerting'; import { RulerRulesConfigDTO } from '../../../types/unified-alerting-dto'; import { Origin } from './components/rule-viewer/tabs/version-history/ConfirmVersionRestoreModal'; -import { AdvancedFilters } from './components/rules/Filter/RulesFilter.v2'; import { FilterType } from './components/rules/central-state-history/EventListSceneObject'; -import { RulesFilter, getSearchFilterFromQuery } from './search/rulesSearchParser'; +import { AdvancedFilters } from './rule-list/filter/types'; +import { RulesFilter } from './search/rulesSearchParser'; import { RuleFormType } from './types/rule-form'; export const LogMessages = { @@ -245,44 +245,6 @@ export const trackImportToGMAError = async (payload: { importSource: 'yaml' | 'd reportInteraction('grafana_alerting_import_to_gma_error', { ...payload }); }; -interface RulesSearchInteractionPayload { - filter: string; - triggeredBy: 'typing' | 'component'; -} - -function trackRulesSearchInteraction(payload: RulesSearchInteractionPayload) { - reportInteraction('grafana_alerting_rules_search', { ...payload }); -} - -export function trackRulesSearchInputInteraction({ oldQuery, newQuery }: { oldQuery: string; newQuery: string }) { - try { - const oldFilter = getSearchFilterFromQuery(oldQuery); - const newFilter = getSearchFilterFromQuery(newQuery); - - const oldFilterTerms = extractFilterKeys(oldFilter); - const newFilterTerms = extractFilterKeys(newFilter); - - const newTerms = newFilterTerms.filter((term) => !oldFilterTerms.includes(term)); - newTerms.forEach((term) => { - trackRulesSearchInteraction({ filter: term, triggeredBy: 'typing' }); - }); - } catch (e: unknown) { - if (e instanceof Error) { - logError(e); - } - } -} - -function extractFilterKeys(filter: RulesFilter) { - return Object.entries(filter) - .filter(([_, value]) => !isEmpty(value)) - .map(([key]) => key); -} - -export function trackRulesSearchComponentInteraction(filter: keyof RulesFilter) { - trackRulesSearchInteraction({ filter, triggeredBy: 'component' }); -} - export function trackRulesListViewChange(payload: { view: string }) { reportInteraction('grafana_alerting_rules_list_mode', { ...payload }); } @@ -335,26 +297,64 @@ export function trackFilterButtonClick() { reportInteraction('grafana_alerting_filter_button_click'); } +export function trackAlertRuleFilterEvent( + payload: + | { filterMethod: 'search-input'; filter: RulesFilter } + | { filterMethod: 'filter-component'; filter: keyof RulesFilter } +) { + if (payload.filterMethod === 'search-input') { + const meaningfulValues = filterMeaningfulValues(payload.filter); + reportInteraction('grafana_alerting_rules_filter', { ...meaningfulValues, filterMethod: 'search-input' }); + return; + } + reportInteraction('grafana_alerting_rules_filter', { filter: payload.filter, filterMethod: 'filter-component' }); +} + +export function trackRulesSearchInputCleared(prev: string, next: string) { + // Only report an explicit clear action when transitioning from non-empty to empty + if (prev !== '' && next === '') { + reportInteraction('grafana_alerting_rules_filter_cleared', { filterMethod: 'search-input' }); + } +} + export function trackFilterButtonApplyClick(payload: AdvancedFilters, pluginsFilterEnabled: boolean) { // Filter out empty/default values before tracking - const meaningfulValues = pickBy(payload, (value, key) => { + const meaningfulValues = filterMeaningfulValues(payload, { pluginsFilterEnabled }); + + reportInteraction('grafana_alerting_rules_filter', { + ...meaningfulValues, + filterMethod: 'filter-component', + }); +} + +function filterMeaningfulValues( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + obj: Record, + opts?: { pluginsFilterEnabled?: boolean } +) { + const { pluginsFilterEnabled = true } = opts ?? {}; + return pickBy(obj, (value, key) => { if (value === null || value === undefined || value === '') { return false; } if (Array.isArray(value) && value.length === 0) { return false; } + if (value === '*') { + return false; + } if (key === 'plugins' && !pluginsFilterEnabled) { return false; } + if (key === 'plugins' && value === 'show') { + return false; + } return true; }); - - reportInteraction('grafana_alerting_filter_button_apply_click', meaningfulValues); } export function trackFilterButtonClearClick() { - reportInteraction('grafana_alerting_filter_button_clear_click'); + reportInteraction('grafana_alerting_rules_filter_cleared', { filterMethod: 'filter-component' }); } export type AlertRuleTrackingProps = { diff --git a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx index a8e3d4d3fba..ab29218ebb0 100644 --- a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx +++ b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx @@ -48,9 +48,11 @@ function FolderCreationModal({ const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); const [title, setTitle] = useState(''); + const [isCreatingFolder, setIsCreatingFolder] = useState(false); const [createFolder] = useNewFolderMutation(); const onSubmit = async () => { + setIsCreatingFolder(true); const { data, error } = await createFolder({ title }); if (error) { @@ -59,6 +61,7 @@ function FolderCreationModal({ onCreate({ title: data.title, uid: data.uid }); notifyApp.success('Folder created'); } + setIsCreatingFolder(false); }; return ( @@ -93,7 +96,7 @@ function FolderCreationModal({
{canRenderContactPointSelector && ( @@ -271,7 +263,10 @@ const RulesFilter = ({ onClear = () => undefined, viewMode, onViewModeChange }: onSubmit={handleSubmit((data) => { setSearchQuery(data.searchQuery); searchQueryRef.current?.blur(); - trackRulesSearchInputInteraction({ oldQuery: searchQuery, newQuery: data.searchQuery }); + trackAlertRuleFilterEvent({ + filterMethod: 'search-input', + filter: getSearchFilterFromQuery(data.searchQuery), + }); })} > (null); - const { pluginsFilterEnabled } = usePluginsFilterStatus(); - - // this form will managed the search query string, which is updated either by the user typing in the input or by the advanced filters - const { setValue, watch, getValues, handleSubmit } = useForm({ - defaultValues: { - query: searchQuery, - }, - }); - - useEffect(() => { - setValue('query', searchQuery); - }, [searchQuery, setValue]); - - const submitHandler: SubmitHandler = (values: SearchQueryForm) => { - const parsedFilter = getSearchFilterFromQuery(values.query); - updateFilters(parsedFilter); - }; - - const handleAdvancedFilters: SubmitHandler = (values) => { - const newFilter = formAdvancedFiltersToRuleFilter(values); - updateFilters(newFilter); - - const newSearchQuery = applySearchFilterToQuery('', newFilter); - setSearchQuery(newSearchQuery); - - trackFilterButtonApplyClick(values, pluginsFilterEnabled); - setIsPopupOpen(false); // Should close popup after applying filters? - }; - - const handleClearFilters = () => { - updateFilters(formAdvancedFiltersToRuleFilter(emptyAdvancedFilters)); - setSearchQuery(undefined); - }; - - const handleOnToggle = () => { - trackFilterButtonClick(); - setIsPopupOpen(!isPopupOpen); - }; - - // Handle outside clicks to close the popup - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (isPopupOpen && popupRef.current && event.target instanceof Node && !popupRef.current.contains(event.target)) { - // Check if click is on a portal element (combobox dropdown) - if (event.target instanceof Element) { - const isPortalClick = - event.target.closest('[data-popper-placement]') || event.target.closest('[role="listbox"]'); - - if (!isPortalClick) { - setIsPopupOpen(false); - } - } else { - setIsPopupOpen(false); - } - } - }; - - if (isPopupOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isPopupOpen]); - - const filterButtonLabel = t('alerting.rules-filter.filter-options.aria-label-show-filters', 'Filter'); - return ( -
{}}> - - - - - setValue('query', string)} - onBlur={() => { - const currentQuery = getValues('query'); - const parsedFilter = getSearchFilterFromQuery(currentQuery); - updateFilters(parsedFilter); - }} - value={watch('query')} - /> - - {/* the popup card is mounted inside of a portal, so we can't rely on the usual form handling mechanisms of button[type=submit] */} - setIsPopupOpen(false)} - onToggle={handleOnToggle} - content={ - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
e.stopPropagation()} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.stopPropagation(); - } - }} - role="dialog" - aria-label={t('alerting.rules-filter.filter-options.aria-label', 'Filter options')} - tabIndex={-1} - > - -
- } - > - -
- -
-
-
- ); -} - -interface FilterOptionsProps { - onSubmit: SubmitHandler; - onClear: () => void; - pluginsFilterEnabled: boolean; -} - -const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOptionsProps) => { - const styles = useStyles2(getStyles); - const theme = useStyles2((theme) => theme); - const { filterState } = useRulesFilter(); - const isManualResetRef = useRef(false); - - // Create portal container to render dropdowns above the popup modal - const portalContainer = usePortalContainer(theme.zIndex.portal + 100); - - const defaultValues = searchQueryToDefaultValues(filterState); - - // Fetch namespace and group data from all sources (optimized for filter UI) - const { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder } = - useNamespaceAndGroupOptions(); - - const { labelOptions, isLoadingGrafanaLabels } = useLabelOptions(); - - // Create label options for the multi-select dropdown - const dataSourceOptions = useAlertingDataSourceOptions(); - - // turn the filterState into form default values - const { handleSubmit, reset, register, control } = useForm({ - defaultValues, - }); - - // Update form values when filterState changes (e.g., when popup reopens) - useEffect(() => { - // Skip if we're in the middle of a manual reset - if (isManualResetRef.current) { - isManualResetRef.current = false; - return; - } - - const newDefaultValues = searchQueryToDefaultValues(filterState); - reset(newDefaultValues); - }, [filterState, reset]); - - const submitAdvancedFilters = handleSubmit(onSubmit); - - return ( -
{ - isManualResetRef.current = true; - reset(emptyAdvancedFilters); - trackFilterButtonClearClick(); - onClear(); - }} - > - -
- - - - ( - field.onChange(selections.map((s) => s.value))} - placeholder={ - isLoadingGrafanaLabels - ? t('common.loading', 'Loading...') - : t('alerting.rules-filter.placeholder-labels', 'Select labels') - } - loading={isLoadingGrafanaLabels} - disabled={isLoadingGrafanaLabels || labelOptions.filter((option) => !option.infoOption).length === 0} - portalContainer={portalContainer} - width="auto" - minWidth={40} - maxWidth={80} - /> - )} - /> - - { - return ( - - placeholder={namespacePlaceholder} - options={namespaceOptions} - onChange={(option) => field.onChange(option?.value || null)} - value={field.value} - loading={isLoadingNamespaces} - disabled={isLoadingNamespaces || namespaceOptions.length === 0} - isClearable - portalContainer={portalContainer} - /> - ); - }} - /> - - { - return ( - - placeholder={groupPlaceholder} - options={allGroupNames.map((name) => ({ label: name, value: name }))} - onChange={(option) => field.onChange(option?.value || null)} - value={field.value} - loading={isLoadingNamespaces} - disabled={isLoadingNamespaces || allGroupNames.length === 0} - isClearable - portalContainer={portalContainer} - /> - ); - }} - /> -
- } - > - - -
- - ( - field.onChange(selections.map((s) => s.value))} - placeholder={t('alerting.rules-filter.placeholder-data-sources', 'Select data sources')} - portalContainer={portalContainer} - width="auto" - minWidth={40} - maxWidth={80} - /> - )} - /> - {canRenderContactPointSelector && ( - <> - - { - return ( - { - field.onChange(contactPoint?.spec.title || null); - }} - portalContainer={portalContainer} - /> - ); - }} - /> - - )} - - ( - - options={[ - { label: t('common.all', 'All'), value: '*' }, - { label: t('alerting.rules.state.firing', 'Firing'), value: PromAlertingRuleState.Firing }, - { label: t('alerting.rules.state.normal', 'Normal'), value: PromAlertingRuleState.Inactive }, - { label: t('alerting.rules.state.pending', 'Pending'), value: PromAlertingRuleState.Pending }, - { - label: t('alerting.rules.state.recovering', 'Recovering'), - value: PromAlertingRuleState.Recovering, - }, - { label: t('alerting.rules.state.unknown', 'Unknown'), value: PromAlertingRuleState.Unknown }, - ]} - value={field.value} - onChange={field.onChange} - /> - )} - /> - - ( - - options={[ - { label: t('common.all', 'All'), value: '*' }, - { label: t('alerting.rules.type.alert', 'Alert rule'), value: PromRuleType.Alerting }, - { label: t('alerting.rules.type.recording', 'Recording rule'), value: PromRuleType.Recording }, - ]} - value={field.value} - onChange={field.onChange} - /> - )} - /> - - ( - - options={[ - { label: t('common.all', 'All'), value: '*' }, - { label: t('alerting.rules.health.ok', 'OK'), value: RuleHealth.Ok }, - { label: t('alerting.rules.health.no-data', 'No data'), value: RuleHealth.NoData }, - { label: t('alerting.rules.health.error', 'Error'), value: RuleHealth.Error }, - ]} - value={field.value} - onChange={field.onChange} - /> - )} - /> - {pluginsFilterEnabled && ( - <> - - ( - - options={[ - { label: t('alerting.rules-filter.label.show', 'Show'), value: 'show' }, - { label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' }, - ]} - value={field.value} - onChange={field.onChange} - /> - )} - /> - - )} - - - - - - - - ); -}; - -function SearchQueryHelp() { - const styles = useStyles2(helpStyles); - - return ( -
-
- - Search syntax allows to query alert rules by the parameters defined below. - -
-
-
-
- Filter type -
-
- Expression -
- - - - - - - - - - -
-
- ); -} - -function HelpRow({ title, expr }: { title: string; expr: string }) { - const styles = useStyles2(helpStyles); - - return ( - <> -
{title}
- {expr} - - ); -} - -const helpStyles = (theme: GrafanaTheme2) => ({ - grid: css({ - display: 'grid', - gridTemplateColumns: 'max-content auto', - gap: theme.spacing(1), - alignItems: 'center', - }), - code: css({ - display: 'block', - textAlign: 'center', - }), -}); - -function getStyles(theme: GrafanaTheme2) { - return { - content: css({ - padding: theme.spacing(1), - }), - grid: css({ - display: 'grid', - gridTemplateColumns: 'auto 1fr', - alignItems: 'center', - gap: theme.spacing(2), - }), - }; -} diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index 7f0ae393755..7018bc3f9c9 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -7,7 +7,6 @@ import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import { GrafanaRulesExporter } from '../components/export/GrafanaRulesExporter'; -import RulesFilter from '../components/rules/Filter/RulesFilter'; import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelector'; import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton'; import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; @@ -17,6 +16,7 @@ import { isAdmin } from '../utils/misc'; import { FilterView } from './FilterView'; import { GroupedView } from './GroupedView'; import { RuleListPageTitle } from './RuleListPageTitle'; +import RulesFilter from './filter/RulesFilter'; function RuleList() { const { filterState } = useRulesFilter(); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.test.tsx similarity index 91% rename from public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx rename to public/app/features/alerting/unified/rule-list/filter/RulesFilter.test.tsx index 720e0ed1275..a0d383f71c6 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.test.tsx @@ -7,13 +7,13 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import * as analytics from '../../Analytics'; import { setupPluginsExtensionsHook } from '../../testSetup/plugins'; -import RulesFilter from './Filter/RulesFilter'; +import RulesFilter from './RulesFilter'; setupMswServer(); jest.spyOn(analytics, 'logInfo'); -jest.mock('./MultipleDataSourcePicker', () => { - const original = jest.requireActual('./MultipleDataSourcePicker'); +jest.mock('../../components/rules/MultipleDataSourcePicker', () => { + const original = jest.requireActual('../../components/rules/MultipleDataSourcePicker'); return { ...original, MultipleDataSourcePicker: () => null, diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.tsx similarity index 77% rename from public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx rename to public/app/features/alerting/unified/rule-list/filter/RulesFilter.tsx index 3eea628410f..c3a159c7ab2 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.tsx @@ -2,8 +2,8 @@ import { Suspense, lazy } from 'react'; import { config } from '@grafana/runtime'; -import RulesFilterV1 from './RulesFilter.v1'; -import { SupportedView } from './RulesViewModeSelector'; +import RulesFilterV1 from '../../components/rules/Filter/RulesFilter.v1'; +import { SupportedView } from '../../components/rules/Filter/RulesViewModeSelector'; const RulesFilterV2 = lazy(() => import('./RulesFilter.v2')); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilterV2.test.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.test.tsx similarity index 76% rename from public/app/features/alerting/unified/components/rules/RulesFilterV2.test.tsx rename to public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.test.tsx index ecff8085f42..18d68d0f670 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilterV2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.test.tsx @@ -13,8 +13,12 @@ import { useRulesFilter } from '../../hooks/useFilteredRules'; import { RulesFilter as RulesFilterType } from '../../search/rulesSearchParser'; import { setupPluginsExtensionsHook } from '../../testSetup/plugins'; +import RulesFilter from './RulesFilter'; + // Grant permission before importing the component since permission check happens at module level grantUserPermissions([AccessControlAction.AlertingReceiversRead]); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const RulesFilterV2 = require('./RulesFilter.v2').default; let mockFilterState: RulesFilterType = { ruleName: '', @@ -40,9 +44,6 @@ jest.mock('../../hooks/useFilteredRules', () => ({ })), })); -import RulesFilter from './Filter/RulesFilter'; -import RulesFilterV2 from './Filter/RulesFilter.v2'; - const useRulesFilterMock = useRulesFilter as jest.MockedFunction; setupMswServer(); @@ -50,6 +51,8 @@ setupMswServer(); jest.spyOn(analytics, 'trackFilterButtonClick'); jest.spyOn(analytics, 'trackFilterButtonApplyClick'); jest.spyOn(analytics, 'trackFilterButtonClearClick'); +jest.spyOn(analytics, 'trackAlertRuleFilterEvent'); +jest.spyOn(analytics, 'trackRulesSearchInputCleared'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -61,8 +64,8 @@ jest.mock('@grafana/runtime', () => ({ }), })); -jest.mock('./MultipleDataSourcePicker', () => { - const original = jest.requireActual('./MultipleDataSourcePicker'); +jest.mock('../../components/rules/MultipleDataSourcePicker', () => { + const original = jest.requireActual('../../components/rules/MultipleDataSourcePicker'); return { ...original, MultipleDataSourcePicker: () => null, @@ -119,10 +122,25 @@ beforeEach(() => { labels: [], }; mockSearchQuery = ''; - mockUpdateFilters.mockClear(); - mockSetSearchQuery.mockClear(); - mockClearAll.mockClear(); + // Fully reset mock implementations between tests to avoid leakage across cases + mockUpdateFilters.mockReset(); + mockSetSearchQuery.mockReset(); + mockClearAll.mockReset(); + mockUpdateFilters.mockImplementation(() => {}); mockSetSearchQuery.mockImplementation(() => {}); + mockClearAll.mockImplementation(() => {}); + + // Restore the default implementation of the hook to use current mock variables + useRulesFilterMock.mockReset(); + useRulesFilterMock.mockImplementation(() => ({ + searchQuery: mockSearchQuery, + filterState: mockFilterState, + updateFilters: mockUpdateFilters, + setSearchQuery: mockSetSearchQuery, + clearAll: mockClearAll, + hasActiveFilters: false, + activeFilters: [], + })); // Reset plugin components hook to default (no plugins) setPluginComponentsHook(() => ({ @@ -209,21 +227,33 @@ describe('RulesFilterV2', () => { }); it('Should populate search field with query string when filters are applied via rule name', async () => { - const { user } = render(); + const { user, rerender } = render(); await user.click(ui.filterButton.get()); await user.type(ui.ruleNameInput.get(), 'test'); - // Mock the setSearchQuery to update mockSearchQuery - mockSetSearchQuery.mockImplementation((newQuery: string | undefined) => { - mockSearchQuery = newQuery ?? ''; + // Mock updateFilters to update the search query as the implementation does + mockUpdateFilters.mockImplementation(() => { + mockSearchQuery = 'rule:test'; }); await user.click(ui.applyButton.get()); - // Check that setSearchQuery was called with the expected query - expect(mockSetSearchQuery).toHaveBeenCalledWith('rule:test'); + // Update the mock to return the new search query and re-render + useRulesFilterMock.mockReturnValue({ + searchQuery: mockSearchQuery, + filterState: mockFilterState, + updateFilters: mockUpdateFilters, + setSearchQuery: mockSetSearchQuery, + clearAll: mockClearAll, + hasActiveFilters: false, + activeFilters: [], + }); + rerender(); + + // The search input should reflect the updated query string + expect(ui.searchInput.get()).toHaveValue('rule:test'); }); it('Should parse search query and call updateFilters when user types directly in search field', async () => { @@ -294,7 +324,7 @@ describe('RulesFilterV2', () => { // Permission is already mocked to true at module level const { user } = render(); await user.click(ui.filterButton.get()); - expect(screen.getByText('Contact point')).toBeInTheDocument(); + expect(await screen.findByText('Contact point')).toBeInTheDocument(); }); it('Should show plugin filter when plugins are enabled', async () => { @@ -306,7 +336,7 @@ describe('RulesFilterV2', () => { const { user } = render(); await user.click(ui.filterButton.get()); - expect(screen.getByText('Plugin rules')).toBeInTheDocument(); + expect(await screen.findByText('Plugin rules')).toBeInTheDocument(); }); it('Should hide plugin filter when no plugins are available', async () => { @@ -349,6 +379,40 @@ describe('RulesFilterV2', () => { expect(analytics.trackFilterButtonApplyClick).toHaveBeenCalledTimes(1); }); + it('Should track search input submit with parsed filter payload', async () => { + const { user } = render(); + + await user.type(ui.searchInput.get(), 'rule:test state:firing'); + await user.keyboard('{Enter}'); + + expect(analytics.trackAlertRuleFilterEvent).toHaveBeenCalled(); + const callArg = (analytics.trackAlertRuleFilterEvent as jest.Mock).mock.calls.at(-1)?.[0]; + expect(callArg.filterMethod).toBe('search-input'); + expect(callArg.filter).toMatchObject({ ruleName: 'test', ruleState: 'firing' }); + }); + + it('Should track search input blur with parsed filter payload', async () => { + const { user } = render(); + + await user.type(ui.searchInput.get(), 'state:firing'); + await user.click(document.body); + + expect(analytics.trackAlertRuleFilterEvent).toHaveBeenCalled(); + const callArg = (analytics.trackAlertRuleFilterEvent as jest.Mock).mock.calls.at(-1)?.[0]; + expect(callArg.filterMethod).toBe('search-input'); + expect(callArg.filter).toMatchObject({ ruleState: 'firing' }); + }); + + it('Should track search input clear when input transitions to empty', async () => { + const { user } = render(); + + await user.type(ui.searchInput.get(), 'abc'); + expect(ui.searchInput.get()).toHaveValue('abc'); + await user.clear(ui.searchInput.get()); + + expect(analytics.trackRulesSearchInputCleared).toHaveBeenCalled(); + }); + it('Should not track filter button click when filter button is clicked to close popup', async () => { const { user } = render(); diff --git a/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx new file mode 100644 index 00000000000..69b3b865a43 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx @@ -0,0 +1,753 @@ +import { css } from '@emotion/css'; +import { useEffect, useRef, useState } from 'react'; +import { Controller, FormProvider, SubmitHandler, useForm, useFormContext } from 'react-hook-form'; + +import { ContactPointSelector } from '@grafana/alerting/unstable'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { + Box, + Button, + Combobox, + FilterInput, + Icon, + Input, + Label, + MultiCombobox, + RadioButtonGroup, + Stack, + Tooltip, + useStyles2, + useTheme2, +} from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; +import type { AdvancedFilters } from 'app/features/alerting/unified/rule-list/filter/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; + +import { + trackAlertRuleFilterEvent, + trackFilterButtonApplyClick, + trackFilterButtonClearClick, + trackFilterButtonClick, + trackRulesSearchInputCleared, +} from '../../Analytics'; +import { PopupCard } from '../../components/HoverCard'; +import { RulesViewModeSelector } from '../../components/rules/Filter/RulesViewModeSelector'; +import { + useAlertingDataSourceOptions, + useLabelOptions, + useNamespaceAndGroupOptions, +} from '../../components/rules/Filter/useRuleFilterAutocomplete'; +import { useRulesFilter } from '../../hooks/useFilteredRules'; +import { RuleHealth, getSearchFilterFromQuery } from '../../search/rulesSearchParser'; + +import { RulesFilterProps } from './RulesFilter'; +import { + emptyAdvancedFilters, + formAdvancedFiltersToRuleFilter, + searchQueryToDefaultValues, + usePluginsFilterStatus, + usePortalContainer, +} from './utils'; + +const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead); + +type SearchQueryForm = { + query: string; +}; + +export default function RulesFilter({ viewMode, onViewModeChange }: RulesFilterProps) { + const styles = useStyles2(getStyles); + + const [isPopupOpen, setIsPopupOpen] = useState(false); + const { searchQuery, updateFilters, setSearchQuery } = useRulesFilter(); + const popupRef = useRef(null); + const { pluginsFilterEnabled } = usePluginsFilterStatus(); + + // this form will managed the search query string, which is updated either by the user typing in the input or by the advanced filters + const { control, setValue, handleSubmit } = useForm({ + defaultValues: { + query: searchQuery, + }, + }); + + useEffect(() => { + setValue('query', searchQuery); + }, [searchQuery, setValue]); + + const submitHandler: SubmitHandler = (values: SearchQueryForm) => { + const parsedFilter = getSearchFilterFromQuery(values.query); + trackAlertRuleFilterEvent({ filterMethod: 'search-input', filter: parsedFilter }); + updateFilters(parsedFilter); + }; + + const handleAdvancedFilters: SubmitHandler = (values) => { + const newFilter = formAdvancedFiltersToRuleFilter(values); + updateFilters(newFilter); + + trackFilterButtonApplyClick(values, pluginsFilterEnabled); + setIsPopupOpen(false); // Should close popup after applying filters? + }; + + const handleClearFilters = () => { + updateFilters(formAdvancedFiltersToRuleFilter(emptyAdvancedFilters)); + setSearchQuery(undefined); + }; + + const handleOnToggle = () => { + trackFilterButtonClick(); + setIsPopupOpen(!isPopupOpen); + }; + + // Handle outside clicks to close the popup + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (isPopupOpen && popupRef.current && event.target instanceof Node && !popupRef.current.contains(event.target)) { + // Check if click is on a portal element (combobox dropdown) + if (event.target instanceof Element) { + const isPortalClick = + event.target.closest('[data-popper-placement]') || event.target.closest('[role="listbox"]'); + + if (!isPortalClick) { + setIsPopupOpen(false); + } + } else { + setIsPopupOpen(false); + } + } + }; + + if (isPopupOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isPopupOpen]); + + const filterButtonLabel = t('alerting.rules-filter.filter-options.aria-label-show-filters', 'Filter'); + return ( +
{}}> + + + + + ( + { + trackRulesSearchInputCleared(field.value, next); + field.onChange(next); + }} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === 'NumpadEnter') { + event.preventDefault(); + handleSubmit(submitHandler)(); + } + }} + onBlur={() => { + const currentQuery = field.value; + const parsedFilter = getSearchFilterFromQuery(currentQuery); + trackAlertRuleFilterEvent({ filterMethod: 'search-input', filter: parsedFilter }); + updateFilters(parsedFilter); + }} + value={field.value} + /> + )} + /> + + {/* the popup card is mounted inside of a portal, so we can't rely on the usual form handling mechanisms of button[type=submit] */} + setIsPopupOpen(false)} + onToggle={handleOnToggle} + content={ + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions +
e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + } + }} + role="dialog" + aria-label={t('alerting.rules-filter.filter-options.aria-label', 'Filter options')} + tabIndex={-1} + > + +
+ } + > + +
+ +
+
+
+ ); +} + +interface FilterOptionsProps { + onSubmit: SubmitHandler; + onClear: () => void; + pluginsFilterEnabled: boolean; +} + +const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOptionsProps) => { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const { filterState } = useRulesFilter(); + const isManualResetRef = useRef(false); + + // Create portal container to render dropdowns above the popup modal + const portalContainer = usePortalContainer(theme.zIndex.portal + 100); + + const defaultValues = searchQueryToDefaultValues(filterState); + + // Fetch namespace and group data from all sources (optimized for filter UI) + const { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder } = + useNamespaceAndGroupOptions(); + + const { labelOptions, isLoadingGrafanaLabels } = useLabelOptions(); + + // Create label options for the multi-select dropdown + const dataSourceOptions = useAlertingDataSourceOptions(); + + // turn the filterState into form default values + const methods = useForm({ + defaultValues, + }); + const { handleSubmit, reset } = methods; + + // Update form values when filterState changes (e.g., when popup reopens) + useEffect(() => { + // Skip if we're in the middle of a manual reset + if (isManualResetRef.current) { + isManualResetRef.current = false; + return; + } + + const newDefaultValues = searchQueryToDefaultValues(filterState); + reset(newDefaultValues); + }, [filterState, reset]); + + const submitAdvancedFilters = handleSubmit(onSubmit); + + return ( + +
{ + isManualResetRef.current = true; + reset(emptyAdvancedFilters); + trackFilterButtonClearClick(); + onClear(); + }} + > + +
+ + + + + + {canRenderContactPointSelector && } + + + + {pluginsFilterEnabled && } +
+ + + + +
+
+
+ ); +}; + +function RuleNameField() { + const { register } = useFormContext(); + return ( + <> + + + + ); +} + +function LabelsField({ + labelOptions, + isLoadingGrafanaLabels, + portalContainer, +}: { + labelOptions: Array<{ label?: string; value: string; infoOption?: boolean }>; + isLoadingGrafanaLabels: boolean; + portalContainer?: HTMLElement; +}) { + const { control } = useFormContext(); + return ( + <> + + ( + field.onChange(selections.map((s) => s.value))} + placeholder={ + isLoadingGrafanaLabels + ? t('common.loading', 'Loading...') + : t('alerting.rules-filter.placeholder-labels', 'Select labels') + } + loading={isLoadingGrafanaLabels} + disabled={isLoadingGrafanaLabels || labelOptions.filter((option) => !option.infoOption).length === 0} + portalContainer={portalContainer} + width="auto" + minWidth={40} + maxWidth={80} + /> + )} + /> + + ); +} + +function NamespaceField({ + namespaceOptions, + namespacePlaceholder, + isLoadingNamespaces, + portalContainer, +}: { + namespaceOptions: Array<{ label?: string; value: string; description?: string }>; + namespacePlaceholder: string; + isLoadingNamespaces: boolean; + portalContainer?: HTMLElement; +}) { + const { control } = useFormContext(); + return ( + <> + + { + return ( + + placeholder={namespacePlaceholder} + options={namespaceOptions} + onChange={(option) => field.onChange(option?.value || null)} + value={field.value} + loading={isLoadingNamespaces} + disabled={isLoadingNamespaces || namespaceOptions.length === 0} + isClearable + portalContainer={portalContainer} + /> + ); + }} + /> + + ); +} + +function GroupField({ + allGroupNames, + groupPlaceholder, + isLoadingNamespaces, + portalContainer, +}: { + allGroupNames: string[]; + groupPlaceholder: string; + isLoadingNamespaces: boolean; + portalContainer?: HTMLElement; +}) { + const { control } = useFormContext(); + return ( + <> + + { + return ( + + placeholder={groupPlaceholder} + options={allGroupNames.map((name) => ({ label: name, value: name }))} + onChange={(option) => field.onChange(option?.value || null)} + value={field.value} + loading={isLoadingNamespaces} + disabled={isLoadingNamespaces || allGroupNames.length === 0} + isClearable + portalContainer={portalContainer} + /> + ); + }} + /> + + ); +} + +function DataSourceNamesField({ + dataSourceOptions, + portalContainer, +}: { + dataSourceOptions: Array<{ label?: string; value: string }>; + portalContainer?: HTMLElement; +}) { + const { control } = useFormContext(); + return ( + <> + + ( + field.onChange(selections.map((s) => s.value))} + placeholder={t('alerting.rules-filter.placeholder-data-sources', 'Select data sources')} + portalContainer={portalContainer} + width="auto" + minWidth={40} + maxWidth={80} + /> + )} + /> + + ); +} + +function ContactPointField({ portalContainer }: { portalContainer?: HTMLElement }) { + const { control } = useFormContext(); + return ( + <> + + { + return ( + { + field.onChange(contactPoint?.spec.title || null); + }} + portalContainer={portalContainer} + /> + ); + }} + /> + + ); +} + +function RuleStateField() { + const { control } = useFormContext(); + return ( + <> + + ( + + options={[ + { label: t('common.all', 'All'), value: '*' }, + { label: t('alerting.rules.state.firing', 'Firing'), value: PromAlertingRuleState.Firing }, + { label: t('alerting.rules.state.normal', 'Normal'), value: PromAlertingRuleState.Inactive }, + { label: t('alerting.rules.state.pending', 'Pending'), value: PromAlertingRuleState.Pending }, + { label: t('alerting.rules.state.recovering', 'Recovering'), value: PromAlertingRuleState.Recovering }, + { label: t('alerting.rules.state.unknown', 'Unknown'), value: PromAlertingRuleState.Unknown }, + ]} + value={field.value} + onChange={field.onChange} + /> + )} + /> + + ); +} + +function RuleTypeField() { + const { control } = useFormContext(); + return ( + <> + + ( + + options={[ + { label: t('common.all', 'All'), value: '*' }, + { label: t('alerting.rules.type.alert', 'Alert rule'), value: PromRuleType.Alerting }, + { label: t('alerting.rules.type.recording', 'Recording rule'), value: PromRuleType.Recording }, + ]} + value={field.value} + onChange={field.onChange} + /> + )} + /> + + ); +} + +function RuleHealthField() { + const { control } = useFormContext(); + return ( + <> + + ( + + options={[ + { label: t('common.all', 'All'), value: '*' }, + { label: t('alerting.rules.health.ok', 'OK'), value: RuleHealth.Ok }, + { label: t('alerting.rules.health.no-data', 'No data'), value: RuleHealth.NoData }, + { label: t('alerting.rules.health.error', 'Error'), value: RuleHealth.Error }, + ]} + value={field.value} + onChange={field.onChange} + /> + )} + /> + + ); +} + +function PluginsField() { + const { control } = useFormContext(); + return ( + <> + + ( + + options={[ + { label: t('alerting.rules-filter.label.show', 'Show'), value: 'show' }, + { label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' }, + ]} + value={field.value} + onChange={field.onChange} + /> + )} + /> + + ); +} + +function SearchQueryHelp() { + const styles = useStyles2(helpStyles); + + return ( +
+
+ + Search syntax allows to query alert rules by the parameters defined below. + +
+
+
+
+ Filter type +
+
+ Expression +
+ + + + + + + + + + +
+
+ ); +} + +function HelpRow({ title, expr }: { title: string; expr: string }) { + const styles = useStyles2(helpStyles); + + return ( + <> +
{title}
+ {expr} + + ); +} + +const helpStyles = (theme: GrafanaTheme2) => ({ + grid: css({ + display: 'grid', + gridTemplateColumns: 'max-content auto', + gap: theme.spacing(1), + alignItems: 'center', + }), + code: css({ + display: 'block', + textAlign: 'center', + }), +}); + +function getStyles(theme: GrafanaTheme2) { + return { + content: css({ + padding: theme.spacing(1), + }), + grid: css({ + display: 'grid', + gridTemplateColumns: 'auto 1fr', + alignItems: 'center', + gap: theme.spacing(2), + }), + }; +} diff --git a/public/app/features/alerting/unified/rule-list/filter/types.ts b/public/app/features/alerting/unified/rule-list/filter/types.ts new file mode 100644 index 00000000000..dbfa6ad59db --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/filter/types.ts @@ -0,0 +1,17 @@ +import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; + +import type { RuleHealth } from '../../search/rulesSearchParser'; + +export type AdvancedFilters = { + namespace?: string | null; + groupName?: string | null; + ruleName?: string; + ruleType?: PromRuleType | '*'; + ruleState: PromAlertingRuleState | '*'; + dataSourceNames: string[]; + labels: string[]; + ruleHealth?: RuleHealth | '*'; + dashboardUid?: string; + plugins?: 'show' | 'hide'; + contactPoint?: string | null; +}; diff --git a/public/app/features/alerting/unified/components/rules/Filter/utils.ts b/public/app/features/alerting/unified/rule-list/filter/utils.ts similarity index 91% rename from public/app/features/alerting/unified/components/rules/Filter/utils.ts rename to public/app/features/alerting/unified/rule-list/filter/utils.ts index 049634634af..798a4b0bade 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/utils.ts +++ b/public/app/features/alerting/unified/rule-list/filter/utils.ts @@ -1,9 +1,9 @@ import { useEffect, useRef } from 'react'; -import { useAlertingHomePageExtensions } from '../../../plugins/useAlertingHomePageExtensions'; -import { RulesFilter } from '../../../search/rulesSearchParser'; +import { useAlertingHomePageExtensions } from '../../plugins/useAlertingHomePageExtensions'; +import { RulesFilter } from '../../search/rulesSearchParser'; -import { AdvancedFilters } from './RulesFilter.v2'; +import { AdvancedFilters } from './types'; export function formAdvancedFiltersToRuleFilter(values: AdvancedFilters): RulesFilter { return { diff --git a/public/app/features/alerting/unified/utils/misc.test.ts b/public/app/features/alerting/unified/utils/misc.test.ts index 8916603f2ee..815d5782a1c 100644 --- a/public/app/features/alerting/unified/utils/misc.test.ts +++ b/public/app/features/alerting/unified/utils/misc.test.ts @@ -196,4 +196,71 @@ describe('stringifyErrorLike', () => { expect(stringifyErrorLike(error)).toBe('POST /my/url failed with 404: not found'); }); + + it('should prioritize data.message over statusText when both are present', () => { + const error = { + status: 500, + data: { message: 'API error message' }, + statusText: 'Internal Server Error', + config: { url: '/api/test', method: 'GET' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('GET /api/test failed with 500: API error message'); + }); + + it('should fall back to statusText when data.message is not available', () => { + const error = { + status: 404, + data: { error: 'some other field' }, + statusText: 'Not Found', + config: { url: '/api/test', method: 'GET' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('Not Found'); + }); + + it('should handle null data safely without crashing', () => { + const error = { + status: 500, + data: null, + statusText: 'Internal Server Error', + config: { url: '/api/test', method: 'POST' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('Internal Server Error'); + }); + + it('should handle undefined data safely without crashing', () => { + const error = { + status: 500, + data: undefined, + statusText: 'Internal Server Error', + config: { url: '/api/test', method: 'PUT' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('Internal Server Error'); + }); + + it('should handle data without message property safely', () => { + const error = { + status: 400, + data: { error: 'validation failed', code: 400 }, + statusText: 'Bad Request', + config: { url: '/api/validate', method: 'POST' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('Bad Request'); + }); + + it('should prioritize error.message over data.message', () => { + const error = { + status: 403, + message: 'Error message property', + data: { message: 'Data message property' }, + statusText: 'Forbidden', + config: { url: '/api/test', method: 'POST' }, + } satisfies FetchError; + + expect(stringifyErrorLike(error)).toBe('Error message property'); + }); }); diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index e7db8710c32..a90ee8f1d40 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -288,6 +288,23 @@ export function isErrorLike(error: unknown): error is Error { return Boolean(error && typeof error === 'object' && 'message' in error); } +// Small composable guards to safely inspect nested shapes without broad assertions +function isObject(value: unknown): value is object { + return typeof value === 'object' && value !== null; +} + +function hasData(value: unknown): value is { data: unknown } { + return isObject(value) && 'data' in value; +} + +function hasMessage(value: unknown): value is { message: string } { + if (!isObject(value)) { + return false; + } + const desc = Object.getOwnPropertyDescriptor(value, 'message'); + return typeof desc?.value === 'string'; +} + export function getErrorCode(error: unknown): string | undefined { if (isApiMachineryError(error) && error.data.details) { return error.data.details.uid; @@ -310,8 +327,7 @@ export function isErrorMatchingCode(error: Error | undefined, code: KnownErrorCo } export function stringifyErrorLike(error: unknown): string { - const fetchError = isFetchError(error); - if (fetchError) { + if (isFetchError(error)) { if (isApiMachineryError(error)) { const message = getErrorMessageFromApiMachineryErrorResponse(error); if (message) { @@ -323,7 +339,8 @@ export function stringifyErrorLike(error: unknown): string { return error.message; } - if ('message' in error.data && typeof error.data.message === 'string') { + // Runtime check for error.data.message without narrow typing - prioritize over statusText + if (hasData(error) && hasMessage(error.data)) { const status = getStatusFromError(error); const message = getMessageFromError(error); diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index a9a0dda6274..b7775e5c249 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -17,7 +17,7 @@ import BrowseDashboardsPage from './BrowseDashboardsPage'; import * as permissions from './permissions'; setupMockServer(); -const [mockTree, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures(); +const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -89,29 +89,6 @@ function render(...[ui, options]: Parameters) { }; } -jest.mock('app/features/browse-dashboards/api/services', () => { - const orig = jest.requireActual('app/features/browse-dashboards/api/services'); - - return { - ...orig, - listFolders(parentUID?: string) { - const childrenForUID = mockTree - .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUID) - .map((v) => v.item); - - return Promise.resolve(childrenForUID); - }, - - listDashboards(parentUID?: string) { - const childrenForUID = mockTree - .filter((v) => v.item.kind === 'dashboard' && v.item.parentUID === parentUID) - .map((v) => v.item); - - return Promise.resolve(childrenForUID); - }, - }; -}); - describe('browse-dashboards BrowseDashboardsPage', () => { const mockPermissions = { canCreateDashboards: true, diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 038a32d1af6..a5e7861a2c9 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -16,6 +16,7 @@ import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo' import { contextSrv } from '../../core/services/context_srv'; import { ManagerKind } from '../apiserver/types'; import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel'; +import { ProvisionedFolderPreviewBanner } from '../provisioning/components/Folders/ProvisionedFolderPreviewBanner'; import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResourceRepositoryView'; import { useSearchStateManager } from '../search/state/SearchStateManager'; import { getSearchPlaceholder } from '../search/tempI18nPhrases'; @@ -26,7 +27,6 @@ import { BrowseFilters } from './components/BrowseFilters'; import { BrowseView } from './components/BrowseView'; import CreateNewButton from './components/CreateNewButton'; import { FolderActionsButton } from './components/FolderActionsButton'; -import { ProvisionedFolderPreviewBanner } from './components/ProvisionedFolderPreviewBanner'; import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; import { useHasSelection } from './state/hooks'; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 5e538d6e91b..ad0a72df516 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -5,6 +5,9 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Stack } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { ManagerKind } from 'app/features/apiserver/types'; +import { BulkDeleteProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource'; +import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource'; +import { useSelectionProvisioningStatus } from 'app/features/provisioning/hooks/useSelectionProvisioningStatus'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; import { ShowModalReactEvent } from 'app/types/events'; import { FolderDTO } from 'app/types/folders'; @@ -14,13 +17,10 @@ import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDa import { useActionSelectionState } from '../../state/hooks'; import { setAllSelection } from '../../state/slice'; import { DashboardTreeSelection } from '../../types'; -import { BulkDeleteProvisionedResource } from '../BulkActions/BulkDeleteProvisionedResource'; -import { BulkMoveProvisionedResource } from '../BulkActions/BulkMoveProvisionedResource'; import { DeleteModal } from './DeleteModal'; import { MoveModal } from './MoveModal'; import { SelectedMixResourcesMsgModal } from './SelectedMixResourcesMsgModal'; -import { useSelectionProvisioningStatus } from './useSelectionProvisioningStatus'; export interface Props { folderDTO?: FolderDTO; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/utils.ts b/public/app/features/browse-dashboards/components/BrowseActions/utils.ts index 59a3979cf2f..2d6b4328c53 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/utils.ts +++ b/public/app/features/browse-dashboards/components/BrowseActions/utils.ts @@ -1,8 +1,4 @@ import { t } from '@grafana/i18n'; -import { DashboardViewItem } from 'app/features/search/types'; - -import { findItem } from '../../state/utils'; -import { DashboardViewItemCollection } from '../../types'; export function buildBreakdownString( folderCount: number, @@ -30,40 +26,3 @@ export function buildBreakdownString( } return breakdownString; } - -// Utility: Get root folder for any item (reusing existing pattern from reducers.ts) -export function getItemRootFolder( - item: { uid: string; parentUID?: string; kind?: string }, - browseState: { - rootItems?: { items: DashboardViewItem[] }; - childrenByParentUID: Record; - } -): string | undefined { - const rootItems = browseState.rootItems?.items || []; - - // If it's already a root-level item, return its UID (only for folders) - if (!item.parentUID) { - return item.kind === 'folder' ? item.uid : undefined; - } - - // For nested items, traverse up to find root folder (same pattern as reducers.ts) - let nextParentUID = item.parentUID; - - while (nextParentUID) { - const parent = findItem(rootItems, browseState.childrenByParentUID, nextParentUID); - - // Safety check to prevent infinite loops (same as reducers.ts) - if (!parent) { - break; - } - - // Found the root folder (no parent) - if (!parent.parentUID) { - return parent.uid; - } - - nextParentUID = parent.parentUID; - } - - return undefined; -} diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index 929f5bbf53c..d9e7fa6c806 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -1,42 +1,21 @@ -import { getByLabelText, render as rtlRender, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { getByLabelText, render, screen, userEvent } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; import { getFolderFixtures } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { contextSrv } from 'app/core/services/context_srv'; import { DashboardViewItem } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { BrowseView } from './BrowseView'; const [mockTree, { folderA, folderA_folderA, folderA_folderB, folderA_folderB_dashbdB, dashbdD, folderB_empty }] = getFolderFixtures(); -function render(...[ui, options]: Parameters) { - rtlRender({ui}, options); -} - -jest.mock('app/features/browse-dashboards/api/services', () => { - const orig = jest.requireActual('app/features/browse-dashboards/api/services'); - - return { - ...orig, - listFolders(parentUID?: string) { - const childrenForUID = mockTree - .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUID) - .map((v) => v.item); - - return Promise.resolve(childrenForUID); - }, - - listDashboards(parentUID?: string) { - const childrenForUID = mockTree - .filter((v) => v.item.kind === 'dashboard' && v.item.parentUID === parentUID) - .map((v) => v.item); - - return Promise.resolve(childrenForUID); - }, - }; -}); +setBackendSrv(backendSrv); +setupMockServer(); describe('browse-dashboards BrowseView', () => { const WIDTH = 800; @@ -48,13 +27,12 @@ describe('browse-dashboards BrowseView', () => { canDeleteDashboards: true, }; - afterEach(() => { - // Reset permissions back to defaults - Object.assign(mockPermissions, { - canEditFolders: true, - canEditDashboards: true, - canDeleteFolders: true, - canDeleteDashboards: true, + beforeEach(() => { + jest.spyOn(contextSrv, 'hasPermission').mockImplementation((permission: string) => { + if (permission === AccessControlAction.FoldersRead) { + return true; + } + return false; }); }); @@ -63,7 +41,7 @@ describe('browse-dashboards BrowseView', () => { await screen.findByText(folderA.item.title); await expandFolder(folderA.item); - expect(screen.queryByText(folderA_folderA.item.title)).toBeInTheDocument(); + expect(screen.getByText(folderA_folderA.item.title)).toBeInTheDocument(); await collapseFolder(folderA.item); expect(screen.queryByText(folderA_folderA.item.title)).not.toBeInTheDocument(); @@ -169,13 +147,20 @@ describe('browse-dashboards BrowseView', () => { }); it('shows a simple message if the user has viewer rights', async () => { - mockPermissions.canEditFolders = false; - mockPermissions.canEditDashboards = false; - mockPermissions.canDeleteFolders = false; - mockPermissions.canDeleteDashboards = false; + const mockPermissionsDisabled = { + canEditFolders: false, + canEditDashboards: false, + canDeleteFolders: false, + canDeleteDashboards: false, + }; render( - + ); expect(await screen.findByText('This folder is empty')).toBeInTheDocument(); }); diff --git a/public/app/features/browse-dashboards/components/CheckboxCell.tsx b/public/app/features/browse-dashboards/components/CheckboxCell.tsx index d232a3a4620..c86fd681c95 100644 --- a/public/app/features/browse-dashboards/components/CheckboxCell.tsx +++ b/public/app/features/browse-dashboards/components/CheckboxCell.tsx @@ -6,12 +6,12 @@ import { t } from '@grafana/i18n'; import { Checkbox, Tooltip, useStyles2 } from '@grafana/ui'; import { ManagerKind } from 'app/features/apiserver/types'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; +import { useSelectionRepoValidation } from 'app/features/provisioning/hooks/useSelectionRepoValidation'; import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository'; import { useSelector } from 'app/types/store'; import { DashboardsTreeCellProps, SelectionState } from '../types'; -import { useSelectionRepoValidation } from './BrowseActions/useSelectionRepoValidation'; import { isSharedWithMe, canEditItemType } from './utils'; export default function CheckboxCell({ diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index bc8b896b8ae..0eae387ae58 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -6,6 +6,7 @@ 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 { RepoType } from 'app/features/provisioning/Wizard/types'; +import { NewProvisionedFolderForm } from 'app/features/provisioning/components/Folders/NewProvisionedFolderForm'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository'; import { @@ -20,7 +21,6 @@ import { ManagerKind } from '../../apiserver/types'; import { useNewFolderMutation } from '../api/browseDashboardsAPI'; import { NewFolderForm } from './NewFolderForm'; -import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; interface Props { parentFolder?: FolderDTO; @@ -123,7 +123,11 @@ export default function CreateNewButton({ {parentFolder?.managedBy === ManagerKind.Repo || isProvisionedInstance ? ( setShowNewFolderDrawer(false)} parentFolder={parentFolder} /> ) : ( - setShowNewFolderDrawer(false)} /> + setShowNewFolderDrawer(false)} + parentFolder={parentFolder} + /> )} )} diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx index e8109397870..3e1b597405a 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx @@ -1,24 +1,16 @@ -import { render as rtlRender, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TestProvider } from 'test/helpers/TestProvider'; import { assertIsDefined } from 'test/helpers/asserts'; +import { render, screen } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; -import { - sharedWithMeFolder, - wellFormedDashboard, - wellFormedEmptyFolder, - wellFormedFolder, -} from '../fixtures/dashboardsTreeItem.fixture'; +import { sharedWithMeFolder } from '../fixtures/dashboardsTreeItem.fixture'; import { SelectionState } from '../types'; import { DashboardsTree } from './DashboardsTree'; -function render(...[ui, options]: Parameters) { - rtlRender({ui}, options); -} +const [_, { folderA: folder, folderB_empty: emptyFolderIndicator, dashbdD: dashboard }] = getFolderFixtures(); describe('browse-dashboards DashboardsTree', () => { const WIDTH = 800; @@ -30,9 +22,6 @@ describe('browse-dashboards DashboardsTree', () => { canDeleteDashboards: true, }; - const folder = wellFormedFolder(1); - const emptyFolderIndicator = wellFormedEmptyFolder(); - const dashboard = wellFormedDashboard(2); const noop = () => {}; const isSelected = () => SelectionState.Unselected; const allItemsAreLoaded = () => true; @@ -67,8 +56,8 @@ describe('browse-dashboards DashboardsTree', () => { requestLoadMore={requestLoadMore} /> ); - expect(screen.queryByText(dashboard.item.title)).toBeInTheDocument(); - expect(screen.queryByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument(); + expect(screen.getByText(dashboard.item.title)).toBeInTheDocument(); + expect(screen.getByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument(); expect(screen.getByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashboard.item.uid))).toBeInTheDocument(); }); @@ -113,7 +102,7 @@ describe('browse-dashboards DashboardsTree', () => { /> ); - expect(screen.queryByText(folder.item.title)).toBeInTheDocument(); + expect(screen.getByText(folder.item.title)).toBeInTheDocument(); }); it('renders a folder link', () => { @@ -181,7 +170,7 @@ describe('browse-dashboards DashboardsTree', () => { it('calls onFolderClick when a folder button is clicked', async () => { const handler = jest.fn(); - render( + const { user } = render( { /> ); const folderButton = screen.getByLabelText(`Expand folder ${folder.item.title}`); - await userEvent.click(folderButton); + await user.click(folderButton); expect(handler).toHaveBeenCalledWith(folder.item.uid, true); }); @@ -216,6 +205,6 @@ describe('browse-dashboards DashboardsTree', () => { requestLoadMore={requestLoadMore} /> ); - expect(screen.queryByText('No items')).toBeInTheDocument(); + expect(screen.getByText('No items')).toBeInTheDocument(); }); }); diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index c58b310e0e3..41d877a77c3 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -7,6 +7,7 @@ import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Permissions } from 'app/core/components/AccessControl'; import { appEvents } from 'app/core/core'; import { RepoType } from 'app/features/provisioning/Wizard/types'; +import { DeleteProvisionedFolderForm } from 'app/features/provisioning/components/Folders/DeleteProvisionedFolderForm'; import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository'; import { ShowModalReactEvent } from 'app/types/events'; import { FolderDTO } from 'app/types/folders'; @@ -18,7 +19,6 @@ import { getFolderPermissions } from '../permissions'; import { DeleteModal } from './BrowseActions/DeleteModal'; import { MoveModal } from './BrowseActions/MoveModal'; -import { DeleteProvisionedFolderForm } from './DeleteProvisionedFolderForm'; interface Props { folder: FolderDTO; diff --git a/public/app/features/browse-dashboards/components/NewFolderForm.tsx b/public/app/features/browse-dashboards/components/NewFolderForm.tsx index a5c8fdb1043..1f71e904c99 100644 --- a/public/app/features/browse-dashboards/components/NewFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewFolderForm.tsx @@ -3,12 +3,14 @@ import { useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Button, Input, Field, Stack } from '@grafana/ui'; +import { FolderDTO } from 'app/types/folders'; import { validationSrv } from '../../manage-dashboards/services/ValidationSrv'; interface Props { onConfirm: (folderName: string) => void; onCancel: () => void; + parentFolder?: FolderDTO; } interface FormModel { @@ -17,11 +19,11 @@ interface FormModel { const initialFormModel: FormModel = { folderName: '' }; -export function NewFolderForm({ onCancel, onConfirm }: Props) { +export function NewFolderForm({ onCancel, onConfirm, parentFolder }: Props) { const { handleSubmit, register, - formState: { errors }, + formState: { errors, isSubmitting }, } = useForm({ defaultValues: initialFormModel }); const translatedFolderNameRequiredPhrase = t( @@ -48,7 +50,7 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { defaultValue={initialFormModel.folderName} {...register('folderName', { required: translatedFolderNameRequiredPhrase, - validate: async (v) => await validateFolderName(v), + validate: async (v) => await validateFolderName(v, parentFolder?.uid), })} />
@@ -56,7 +58,7 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { - @@ -64,9 +66,9 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { ); } -export async function validateFolderName(folderName: string) { +export async function validateFolderName(folderName: string, parentFolderUid?: string) { try { - await validationSrv.validateNewFolderName(folderName); + await validationSrv.validateNewFolderName(folderName, parentFolderUid); return true; } catch (e) { if (e instanceof Error) { diff --git a/public/app/features/browse-dashboards/components/utils.ts b/public/app/features/browse-dashboards/components/utils.ts index de03451146f..818b96ebba7 100644 --- a/public/app/features/browse-dashboards/components/utils.ts +++ b/public/app/features/browse-dashboards/components/utils.ts @@ -1,17 +1,8 @@ import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { ManagerKind } from 'app/features/apiserver/types'; -import { DashboardViewItem } from 'app/features/search/types'; +import { ResourceRef } from 'app/features/provisioning/components/BulkActions/useBulkActionJob'; -import { findItem } from '../state/utils'; -import { - DashboardTreeSelection, - DashboardViewItemWithUIItems, - BrowseDashboardsPermissions, - BrowseDashboardsState, -} from '../types'; - -import { ResourceRef } from './BulkActions/useBulkActionJob'; +import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types'; export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) { return baseId + item.uid; @@ -69,35 +60,3 @@ export function canSelectItems(permissions: BrowseDashboardsPermissions) { const canSelectDashboards = canEditDashboards || canDeleteDashboards; return Boolean(canSelectFolders || canSelectDashboards); } - -/** - * Finds the repository name for an item by traversing up the tree to find the root provisioned folder (managed by ManagerKind.Repo) - * This should be an edge case where user have multiple provisioned folders and try to managing resources on root folder - */ -export function getItemRepositoryUid( - item: DashboardViewItem, - rootItems: DashboardViewItem[], - childrenByParentUID: BrowseDashboardsState['childrenByParentUID'] -): string { - // For root provisioned folders, the UID is the repository name - if (item.managedBy === ManagerKind.Repo && !item.parentUID && item.kind === 'folder') { - return item.uid; - } - - // Traverse up the tree to find the root provisioned folder - let currentItem = item; - while (currentItem.parentUID) { - const parent = findItem(rootItems, childrenByParentUID, currentItem.parentUID); - if (!parent) { - break; - } - - if (parent.managedBy === ManagerKind.Repo && !parent.parentUID) { - return currentItem.parentUID; - } - - currentItem = parent; - } - - return 'non_provisioned'; -} diff --git a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts index 7b5d17a2825..f9e12c3f80f 100644 --- a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts @@ -1,28 +1,12 @@ import { Chance } from 'chance'; -import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { DashboardViewItem } from 'app/features/search/types'; -import { DashboardsTreeItem, UIDashboardViewItem } from '../types'; - -export function wellFormedEmptyFolder( - seed = 1, - partial?: Partial> -): DashboardsTreeItem { - const random = Chance(seed); - - return { - item: { - kind: 'ui', - uiKind: 'empty-folder', - uid: random.guid(), - }, - level: 0, - isOpen: false, - ...partial, - }; -} +import { DashboardsTreeItem } from '../types'; +/** + * @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general) + */ export function wellFormedDashboard( seed = 1, partial?: Partial>, @@ -44,6 +28,9 @@ export function wellFormedDashboard( }; } +/** + * @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general) + */ export function wellFormedFolder( seed = 1, partial?: Partial>, @@ -66,6 +53,9 @@ export function wellFormedFolder( }; } +/** + * @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general) + */ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem { const folder = wellFormedFolder(seed, undefined, { uid: 'sharedwithme', @@ -73,15 +63,3 @@ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem getScalarDimensionFromData(this.data, scalar), getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text), getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res), + getDirection: (direction: DirectionDimensionConfig) => getDirectionDimensionFromData(this.data, direction), getPanelData: () => this.data, }; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx index 8d0a0614778..796987d8773 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; - import { t } from '@grafana/i18n'; import { Icon, Stack, Tooltip } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; @@ -56,7 +54,6 @@ export function useConditionalRenderingEditor( }).addItem( new OptionsPaneItemDescriptor({ title, - id: uuidv4(), render: () => , }) ); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 730c16aa410..93e5ec503d8 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -1,5 +1,4 @@ import { ReactNode, useMemo, useRef } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; @@ -39,8 +38,8 @@ export class DashboardEditableElement implements EditableDashboardElement { const { body } = dashboard.useState(); const dashboardOptions = useMemo(() => { - const dashboardTitleInputId = uuidv4(); - const dashboardDescriptionInputId = uuidv4(); + const dashboardTitleInputId = 'dashboard-title-input'; + const dashboardDescriptionInputId = 'dashboard-description-input'; const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' }) .addItem( new OptionsPaneItemDescriptor({ diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index c5c55bbaf61..574028a75d8 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; @@ -49,14 +48,13 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: '', - id: uuidv4(), render: () => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.title-option', 'Title'), - id: uuidv4(), + id: 'PanelFrameTitle', value: panel.state.title, popularRank: 1, render: (descriptor) => ( @@ -67,7 +65,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.description', 'Description'), - id: uuidv4(), + id: 'description-text-area', value: panel.state.description, render: (descriptor) => , }) @@ -75,7 +73,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.transparent-background', 'Transparent background'), - id: uuidv4(), + id: 'transparent-background', render: (descriptor) => , }) ); @@ -135,7 +133,9 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc } } -type OpenPanelEditVizProps = { panel: VizPanel }; +type OpenPanelEditVizProps = { + panel: VizPanel; +}; const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => { return ( diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index d7eb249f1c7..4aa7e2e8987 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -12,10 +12,10 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler'; +import { DashboardPreviewBanner } from 'app/features/provisioning/components/Dashboards/DashboardPreviewBanner'; import { DashboardRoutes } from 'app/types/dashboard'; import { DashboardPrompt } from '../saving/DashboardPrompt'; -import { DashboardPreviewBanner } from '../saving/provisioned/DashboardPreviewBanner'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index a46c113c7c9..366fcdc81e4 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { CoreApp } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -38,7 +37,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'), - id: uuidv4(), + id: 'PanelFrameTitle', value: panel.state.title, popularRank: 1, render: function renderTitle(descriptor) { @@ -56,7 +55,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'), - id: uuidv4(), + id: 'description-text-area', value: panel.state.description, render: function renderDescription(descriptor) { return ; @@ -72,7 +71,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'), - id: uuidv4(), + id: 'transparent-background', render: function renderTransparent(descriptor) { return ; }, @@ -87,7 +86,6 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'), - id: uuidv4(), render: () => , }) ) diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 00001fac1f8..cc437b74486 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -7,10 +7,10 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; +import { getProvisionedMeta } from 'app/features/provisioning/components/utils/getProvisionedMeta'; import { DashboardScene } from '../scene/DashboardScene'; -import { getProvisionedMeta } from './provisioned/utils/getProvisionedMeta'; import { DashboardChangeInfo, NameAlreadyExistsError, SaveButton, isNameExistsError } from './shared'; import { useSaveDashboard } from './useSaveDashboard'; diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx index 8d8e8f1fe13..10be6f94900 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx @@ -2,6 +2,7 @@ import { t } from '@grafana/i18n'; import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef } from '@grafana/scenes'; import { Drawer, Tab, TabsBar } from '@grafana/ui'; import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff'; +import { SaveProvisionedDashboard } from 'app/features/provisioning/components/Dashboards/SaveProvisionedDashboard'; import { useIsProvisionedNG } from 'app/features/provisioning/hooks/useIsProvisionedNG'; import { DashboardScene } from '../scene/DashboardScene'; @@ -9,7 +10,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { SaveDashboardAsForm } from './SaveDashboardAsForm'; import { SaveDashboardForm } from './SaveDashboardForm'; import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm'; -import { SaveProvisionedDashboard } from './provisioned/SaveProvisionedDashboard'; interface SaveDashboardDrawerState extends SceneObjectState { dashboardRef: SceneObjectRef; diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index fece190d860..0eb70a1fe3b 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -6,7 +6,6 @@ import { config, isFetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Box, Button, Stack } from '@grafana/ui'; -import { WorkflowOption } from 'app/features/provisioning/types'; import { Diffs } from '../settings/version-history/utils'; @@ -24,23 +23,6 @@ export interface DashboardChangeInfo { hasMigratedToV2?: boolean; } -export interface BaseProvisionedFormData { - ref?: string; - path: string; - comment?: string; - repo: string; - workflow?: WorkflowOption; - title: string; -} - -export interface ProvisionedDashboardFormData extends BaseProvisionedFormData { - description: string; - folder: { - uid?: string; - title?: string; - }; -} - export function isVersionMismatchError(error?: Error) { return isFetchError(error) && error.data && error.data.status === 'version-mismatch'; } diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx index 93804685fcb..dc20ec7a8bd 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; - import { t } from '@grafana/i18n'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -18,7 +16,7 @@ export function getOptions(model: AutoGridItem): OptionsPaneCategoryDescriptor[] }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.auto-grid.item-options.repeat.variable.title', 'Repeat by variable'), - id: uuidv4(), + id: 'repeat-by-variable-select', description: t( 'dashboard.auto-grid.item-options.repeat.variable.description', 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx index 9f822fe01e7..43929dd09c6 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -23,7 +22,7 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'), - id: uuidv4(), + id: 'repeat-by-variable-select', description: t( 'dashboard.default-layout.item-options.repeat.variable.description', 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' @@ -44,12 +43,11 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'), - id: uuidv4(), useShowIf: () => { const { variableName, repeatDirection } = gridItem.useState(); return Boolean(variableName) && repeatDirection === 'h'; }, - render: (descriptor) => , + render: () => , }) ); @@ -92,7 +90,7 @@ function RepeatDirectionOption({ gridItem }: OptionComponentProps) { ); } -function MaxPerRowOption({ gridItem, id }: OptionComponentProps & { id?: string }) { +function MaxPerRowOption({ gridItem }: OptionComponentProps) { const { maxPerRow } = gridItem.useState(); const maxPerRowOptions: Array> = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), @@ -101,7 +99,6 @@ function MaxPerRowOption({ gridItem, id }: OptionComponentProps & { id?: string return ( row.setState({ title: e.currentTarget.value })} />; + return row.setState({ title: e.currentTarget.value })} />; } -function RowRepeatSelect({ row, dashboard, id }: { row: SceneGridRow; dashboard: DashboardScene; id?: string }) { +function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: DashboardScene }) { const { $behaviors, children } = row.useState(); let repeatBehavior = $behaviors?.find((b) => b instanceof RowRepeaterBehavior); const vizPanels = useMemo( @@ -107,7 +104,6 @@ function RowRepeatSelect({ row, dashboard, id }: { row: SceneGridRow; dashboard: return ( <> { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index a05393bca37..396e4075587 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -1,5 +1,4 @@ -import { useId, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; +import { useMemo } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; @@ -33,15 +32,13 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.rows-layout.row-options.row.fill-screen', 'Fill screen'), - id: uuidv4(), - render: (descriptor) => , + render: () => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.rows-layout.row-options.row.hide-header', 'Hide row header'), - id: uuidv4(), - render: (descriptor) => , + render: () => , }) ), [model, isNewElement] @@ -56,12 +53,11 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat by variable'), - id: uuidv4(), description: t( 'dashboard.rows-layout.row-options.repeat.variable.description', 'Repeat this row for each value in the selected variable.' ), - render: (descriptor) => , + render: () => , }) ), [model] @@ -98,7 +94,6 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool } > row.onHeaderHiddenToggle()} />; + return row.onHeaderHiddenToggle()} />; } -function FillScreenSwitch({ row, id }: { row: RowItem; id?: string }) { +function FillScreenSwitch({ row }: { row: RowItem }) { const { fillScreen } = row.useState(); - return row.onChangeFillScreen(!fillScreen)} />; + return row.onChangeFillScreen(!fillScreen)} />; } -function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) { +function RowRepeatSelect({ row }: { row: RowItem }) { const { layout } = row.useState(); const dashboard = useDashboard(row); @@ -136,7 +131,6 @@ function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) { return ( <> row.onChangeRepeat(repeat)} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 0c34c8a7776..0a2b0b3f30e 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; @@ -25,8 +24,7 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'), - id: uuidv4(), - render: (descriptor) => , + render: () => , }) ), [model, isNewElement] @@ -41,12 +39,11 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.tabs-layout.tab-options.repeat.variable.title', 'Repeat by variable'), - id: uuidv4(), description: t( 'dashboard.tabs-layout.tab-options.repeat.variable.description', 'Repeat this tab for each value in the selected variable.' ), - render: (descriptor) => , + render: () => , }) ), [model] @@ -68,7 +65,7 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa return editOptions; } -function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: boolean; id?: string }) { +function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: boolean }) { const { title } = tab.useState(); const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement }); @@ -82,7 +79,6 @@ function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: } > tab.onChangeRepeat(repeat)} diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx index 1a902f78031..5f88d334eae 100644 --- a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -4,12 +4,11 @@ import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Modal, Space, Text, TextLink } from '@grafana/ui'; +import { DeleteProvisionedDashboardDrawer } from 'app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardDrawer'; import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; import { DashboardScene } from '../scene/DashboardScene'; -import { DeleteProvisionedDashboardDrawer } from './DeleteProvisionedDashboardDrawer'; - interface ButtonProps { dashboard: DashboardScene; } diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index ad47d202db9..aee4461f43e 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -23,6 +23,7 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; import { GenAIDashDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIDashDescriptionButton'; import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/GenAIDashTitleButton'; +import { MoveProvisionedDashboardDrawer } from 'app/features/provisioning/components/Dashboards/MoveProvisionedDashboardDrawer'; import { updateNavModel } from '../pages/utils'; import { DashboardScene } from '../scene/DashboardScene'; @@ -31,7 +32,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { DeleteDashboardButton } from './DeleteDashboardButton'; -import { MoveProvisionedDashboardDrawer } from './MoveProvisionedDashboardDrawer'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; export interface GeneralSettingsEditViewState extends DashboardEditViewState { diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts index 741830b8ec5..88011f08b00 100644 --- a/public/app/features/dashboard-scene/settings/utils.ts +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -111,30 +111,3 @@ export function createDashboardEditViewFor(editview: string): DashboardEditView return new GeneralSettingsEditView({}); } } - -export type ResourceBranchUrlOptions = { - baseUrl?: string; - paramName?: string; - paramValue?: string; - repoType?: string; -}; - -export function buildResourceBranchRedirectUrl({ - baseUrl = '/dashboards', - paramName, - paramValue, - repoType, -}: ResourceBranchUrlOptions): string { - const params = new URLSearchParams(); - - if (paramName && paramValue) { - params.set(paramName, paramValue); - } - - if (repoType) { - params.set('repo_type', repoType); - } - - const queryString = params.toString(); - return queryString ? `${baseUrl}?${queryString}` : baseUrl; -} diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index 862a51ee1ac..86450b91f92 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -1,5 +1,4 @@ -import { FormEvent, useId, useMemo, useRef, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; +import { FormEvent, useMemo, useRef, useState } from 'react'; import { VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -64,16 +63,14 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.variable.label', 'Label'), - id: uuidv4(), description: t('dashboard.edit-pane.variable.label-description', 'Optional display name'), - render: (descriptor) => , + render: () => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.variable.description', 'Description'), - id: uuidv4(), - render: (descriptor) => , + render: () => , }) ) .addItem( @@ -120,7 +117,6 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc interface VariableInputProps { variable: SceneVariable; - id?: string; } function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) { @@ -142,7 +138,6 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable return ( { @@ -176,13 +171,12 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable ); } -function VariableLabelInput({ variable, id }: VariableInputProps) { +function VariableLabelInput({ variable }: VariableInputProps) { const { label } = variable.useState(); const oldLabel = useRef(label ?? ''); return ( { oldLabel.current = label ?? ''; @@ -207,13 +201,13 @@ function VariableLabelInput({ variable, id }: VariableInputProps) { ); } -function VariableDescriptionTextArea({ variable, id }: VariableInputProps) { +function VariableDescriptionTextArea({ variable }: VariableInputProps) { const { description } = variable.useState(); const oldDescription = useRef(description ?? ''); return (