Merge branch 'v8.4.x' of github.com:grafana/grafana into v8.4.x
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
load('scripts/drone/pipelines/pr.star', 'pr_pipelines')
|
||||
load('scripts/drone/pipelines/main.star', 'main_pipelines')
|
||||
load('scripts/drone/pipelines/docs.star', 'docs_pipelines')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
|
||||
load('scripts/drone/version.star', 'version_branch_pipelines')
|
||||
load('scripts/drone/pipelines/cron.star', 'cronjobs')
|
||||
|
||||
+308
-216
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,18 @@
|
||||
<!-- 8.4.1 START -->
|
||||
|
||||
# 8.4.1 (2022-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
|
||||
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
|
||||
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
|
||||
|
||||
<!-- 8.4.1 END -->
|
||||
<!-- 8.4.0 START -->
|
||||
|
||||
# 8.4.0 (2022-02-16)
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ COPY emails emails
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.17.6-alpine3.15 as go-builder
|
||||
FROM golang:1.17.7-alpine3.15 as go-builder
|
||||
|
||||
RUN apk add --no-cache gcc g++ make
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ COPY emails emails
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.17.6 AS go-builder
|
||||
FROM golang:1.17.7 AS go-builder
|
||||
|
||||
WORKDIR /src/grafana
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
"allowDeclareFields": true
|
||||
}
|
||||
],
|
||||
// added to mitigate https://github.com/babel/babel/issues/14289
|
||||
// package (and following line) can be removed once the issue is fixed and released
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
["@babel/plugin-proposal-object-rest-spread", { "loose": true }],
|
||||
"@babel/plugin-transform-react-constant-elements",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
|
||||
@@ -400,6 +400,9 @@ oauth_auto_login = false
|
||||
# OAuth state max age cookie duration in seconds. Defaults to 600 seconds.
|
||||
oauth_state_cookie_max_age = 600
|
||||
|
||||
# Skip forced assignment of OrgID 1 or 'auto_assign_org_id' for social logins
|
||||
oauth_skip_org_role_update_sync = false
|
||||
|
||||
# limit of api_key seconds to live before expiration
|
||||
api_key_max_seconds_to_live = -1
|
||||
|
||||
|
||||
@@ -394,6 +394,9 @@
|
||||
# OAuth state max age cookie duration in seconds. Defaults to 600 seconds.
|
||||
;oauth_state_cookie_max_age = 600
|
||||
|
||||
# Skip forced assignment of OrgID 1 or 'auto_assign_org_id' for social logins
|
||||
;oauth_skip_org_role_update_sync = false
|
||||
|
||||
# limit of api_key seconds to live before expiration
|
||||
;api_key_max_seconds_to_live = -1
|
||||
|
||||
|
||||
@@ -751,6 +751,12 @@ This setting is ignored if multiple OAuth providers are configured. Default is `
|
||||
How many seconds the OAuth state cookie lives before being deleted. Default is `600` (seconds)
|
||||
Administrators can increase this if they experience OAuth login state mismatch errors.
|
||||
|
||||
### oauth_skip_org_role_update_sync
|
||||
|
||||
Skip forced assignment of OrgID `1` or `auto_assign_org_id` for external logins. Default is `false`.
|
||||
Use this setting to distribute users with external login to multiple organizations.
|
||||
Otherwise, the users' organization would get reset on every new login, for example, via AzureAD.
|
||||
|
||||
### api_key_max_seconds_to_live
|
||||
|
||||
Limit of API key seconds to live before expiration. Default is -1 (unlimited).
|
||||
|
||||
@@ -7,7 +7,7 @@ weight = 450
|
||||
|
||||
# Mute timings
|
||||
|
||||
A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. are sent for a policy. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period.
|
||||
A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period.
|
||||
|
||||
Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created.
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ apiVersion: 1
|
||||
datasources:
|
||||
- name: Alertmanager
|
||||
type: alertmanager
|
||||
url: http://localhost:9090
|
||||
url: http://localhost:9093
|
||||
access: proxy
|
||||
jsonData:
|
||||
implementation: 'prometheus' # alternatively 'cortex'
|
||||
|
||||
@@ -32,6 +32,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||
- **Data source -** Target data source.
|
||||
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
|
||||
- **Map tag names -** When enabled, allows configuring how Jaeger tag names map to Loki label names. For example, map `service.name` to `service`.
|
||||
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
|
||||
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
|
||||
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
|
||||
@@ -147,12 +148,15 @@ datasources:
|
||||
tracesToLogs:
|
||||
# Field with internal link pointing to a Loki data source in Grafana.
|
||||
# datasourceUid value must match the `datasourceUid` value of the Loki data source.
|
||||
datasourceUid: loki
|
||||
tags:
|
||||
- cluster
|
||||
- hostname
|
||||
- namespace
|
||||
- pod
|
||||
datasourceUid: 'loki'
|
||||
tags: ['job', 'instance', 'pod', 'namespace']
|
||||
mappedTags: [{ key: 'service.name', value: 'service' }]
|
||||
mapTagNamesEnabled: false
|
||||
spanStartTimeShift: '1h'
|
||||
spanEndTimeShift: '1h'
|
||||
filterByTraceID: false
|
||||
filterBySpanID: false
|
||||
lokiSearch: true
|
||||
secureJsonData:
|
||||
basicAuthPassword: my_password
|
||||
```
|
||||
|
||||
@@ -31,6 +31,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||
- **Data source -** Target data source.
|
||||
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
|
||||
- **Map tag names -** When enabled, allows configuring how Tempo tag names map to Loki label names. For example, map `service.name` to `service`.
|
||||
- **Span start time shift -** A shift in the start time for the Loki query based on the start time for the span. To extend the time to the past, use a negative value. You can use time units, for example, 5s, 1m, 3h. The default is 0.
|
||||
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
|
||||
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
|
||||
@@ -167,6 +168,8 @@ datasources:
|
||||
tracesToLogs:
|
||||
datasourceUid: 'loki'
|
||||
tags: ['job', 'instance', 'pod', 'namespace']
|
||||
mappedTags: [{ key: 'service.name', value: 'service' }]
|
||||
mapTagNamesEnabled: false
|
||||
spanStartTimeShift: '1h'
|
||||
spanEndTimeShift: '1h'
|
||||
filterByTraceID: false
|
||||
|
||||
@@ -32,6 +32,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||
- **Data source -** Target data source.
|
||||
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
|
||||
- **Map tag names -** When enabled, allows configuring how Zipkin tag names map to Loki label names. For example, map `service.name` to `service`.
|
||||
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
|
||||
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
|
||||
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
|
||||
|
||||
@@ -6,4 +6,10 @@ weight = 190
|
||||
|
||||
# Developers
|
||||
|
||||
This section of the documentation contains pages with resources for Grafana developers.
|
||||
This section of the documentation contains pages with resources for Grafana developers:
|
||||
|
||||
- [Grafana Contribution Guide - learn how you can contribute your skills to make Grafana even better.]({{< relref "contribute.md" >}})
|
||||
- [How to build a plugin]({{< relref "plugins" >}})
|
||||
- [Grafana Tutorials - step-by-step guides that help you make the most of Grafana](https://grafana.com/tutorials/)
|
||||
- [Grafana Community Forums - get technical support for open source Grafana, Loki, and Tempo.](https://community.grafana.com)
|
||||
- [Grafana design system - library of reusable Grafana components and guidelines that help you with contribution and plugin development](https://developers.grafana.com)
|
||||
|
||||
@@ -16,10 +16,10 @@ You can choose to encrypt secrets stored in the Grafana database using a key fro
|
||||
|
||||
Grafana integrates with the following key management systems:
|
||||
|
||||
- [AWS KMS]({{< relref "/using-aws-kms-to-encrypt-database-secrets.md" >}})
|
||||
- [Azure Key Vault]({{< relref "/using-azure-key-vault-to-encrypt-database-secrets.md" >}})
|
||||
- [Google Cloud KMS]({{< relref "/using-google-cloud-kms-to-encrypt-database-secrets.md" >}})
|
||||
- [Hashicorp Key Vault]({{< relref "/using-hashicorp-key-vault-to-encrypt-database-secrets.md" >}})
|
||||
- [AWS KMS]({{< relref "using-aws-kms-to-encrypt-database-secrets.md" >}})
|
||||
- [Azure Key Vault]({{< relref "using-azure-key-vault-to-encrypt-database-secrets.md" >}})
|
||||
- [Google Cloud KMS]({{< relref "using-google-cloud-kms-to-encrypt-database-secrets.md" >}})
|
||||
- [Hashicorp Key Vault]({{< relref "using-hashicorp-key-vault-to-encrypt-database-secrets.md" >}})
|
||||
|
||||
Refer to [Database encryption]({{< relref "../../administration/database-encryption.md" >}}) to learn more about how Grafana encrypts secrets in the database.
|
||||
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ keywords = ["grafana", "Hashicorp Vault integration"]
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Using Google Cloud KMS to encrypt database secrets
|
||||
# Using Hashicorp Vault to encrypt database secrets
|
||||
|
||||
You can use an encryption key from Google Cloud Key Management Service to encrypt secrets in the Grafana database.
|
||||
You can use an encryption key from Hashicorp Vault to encrypt secrets in the Grafana database.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
|
||||
@@ -71,6 +71,19 @@ To disable query caching for a single data source:
|
||||
|
||||
To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached.
|
||||
|
||||
## Clear cache
|
||||
|
||||
If you experience performance issues or repeated queries become slower to execute, consider clearing your cache.
|
||||
|
||||
> **Note:** This action impacts all cache-enabled data sources. If you are using Memcached, the system clears all data from the Memcached instance.
|
||||
|
||||
**To clear the cache**:
|
||||
|
||||
1. Sign in to Grafana and click **Settings > Data Sources**.
|
||||
1. Select a data source.
|
||||
1. Click the **Cache** tab.
|
||||
1. Click **Clear cache**.
|
||||
|
||||
## Sending a request without cache
|
||||
|
||||
If a data source query request contains an `X-Cache-Skip` header, then Grafana skips the caching middleware, and does not search the cache for a response. This can be particularly useful when debugging data source queries using cURL.
|
||||
|
||||
@@ -19,7 +19,7 @@ Query parameters:
|
||||
- **dashboardIds** – List of dashboard id's to search for
|
||||
- **folderIds** – List of folder id's to search in for dashboards
|
||||
- **starred** – Flag indicating if only starred Dashboards should be returned
|
||||
- **limit** – Limit the number of returned results (max 5000)
|
||||
- **limit** – Limit the number of returned results (max is 5000; default is 1000)
|
||||
- **page** – Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. Only available in Grafana v6.2+.
|
||||
|
||||
**Example request for retrieving folders and dashboards of the general folder**:
|
||||
|
||||
@@ -8,6 +8,7 @@ weight = 10000
|
||||
Here you can find detailed release notes that list everything that is included in every release as well as notices
|
||||
about deprecations, breaking changes as well as changes that relate to plugin development.
|
||||
|
||||
- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}})
|
||||
- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
|
||||
- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}})
|
||||
- [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}})
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.1"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.1
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
|
||||
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
|
||||
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
|
||||
@@ -69,6 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Modal } from './Modal';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('Modal', () => {
|
||||
it('renders without error', () => {
|
||||
mount(<Modal title={'Some Title'} isOpen={true} />);
|
||||
});
|
||||
|
||||
it('renders nothing by default or when isOpen is false', () => {
|
||||
const wrapper = mount(<Modal title={'Some Title'} />);
|
||||
expect(wrapper.html()).toBe(null);
|
||||
render(<Modal title="Some Title" />);
|
||||
|
||||
wrapper.setProps({ ...wrapper.props(), isOpen: false });
|
||||
expect(wrapper.html()).toBe(null);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correct contents', () => {
|
||||
const wrapper = mount(
|
||||
<Modal title={'Some Title'} isOpen={true}>
|
||||
<div id={'modal-content'}>Content</div>
|
||||
render(
|
||||
<Modal title="Some Title" isOpen>
|
||||
<div data-testid="modal-content">Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(wrapper.find('div#modal-content').length).toBe(1);
|
||||
expect(wrapper.contains('Some Title')).toBeTruthy();
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Some Title')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('modal-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { OverlayContainer } from '@react-aria/overlays';
|
||||
import React, { PropsWithChildren, useCallback, useEffect } from 'react';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
|
||||
import { OverlayContainer, useOverlay } from '@react-aria/overlays';
|
||||
import React, { PropsWithChildren, useRef } from 'react';
|
||||
|
||||
import { useTheme2 } from '../../themes';
|
||||
import { IconName } from '../../types';
|
||||
@@ -39,33 +41,24 @@ export function Modal(props: PropsWithChildren<Props>) {
|
||||
closeOnBackdropClick = true,
|
||||
className,
|
||||
contentClassName,
|
||||
onDismiss: propsOnDismiss,
|
||||
onDismiss,
|
||||
onClickBackdrop,
|
||||
trapFocus = true,
|
||||
} = props;
|
||||
const theme = useTheme2();
|
||||
const styles = getModalStyles(theme);
|
||||
const onDismiss = useCallback(() => {
|
||||
if (propsOnDismiss) {
|
||||
propsOnDismiss();
|
||||
}
|
||||
}, [propsOnDismiss]);
|
||||
|
||||
useEffect(() => {
|
||||
const onEscKey = (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Esc' || ev.key === 'Escape') {
|
||||
onDismiss();
|
||||
}
|
||||
};
|
||||
if (isOpen && closeOnEscape) {
|
||||
document.addEventListener('keydown', onEscKey, false);
|
||||
} else {
|
||||
document.removeEventListener('keydown', onEscKey, false);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onEscKey, false);
|
||||
};
|
||||
}, [closeOnEscape, isOpen, onDismiss]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle interacting outside the dialog and pressing
|
||||
// the Escape key to close the modal.
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ isKeyboardDismissDisabled: closeOnEscape, isOpen, onClose: onDismiss },
|
||||
ref
|
||||
);
|
||||
|
||||
// Get props for the dialog and its title
|
||||
const { dialogProps, titleProps } = useDialog({}, ref);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
@@ -78,16 +71,17 @@ export function Modal(props: PropsWithChildren<Props>) {
|
||||
<div
|
||||
className={styles.modalBackdrop}
|
||||
onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)}
|
||||
{...underlayProps}
|
||||
/>
|
||||
<FocusScope contain={trapFocus} autoFocus restoreFocus>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the modal when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<div tabIndex={-1} className={cx(styles.modal, className)}>
|
||||
<div className={cx(styles.modal, className)} ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<div className={headerClass}>
|
||||
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} />}
|
||||
{typeof title !== 'string' && title}
|
||||
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} id={titleProps.id} />}
|
||||
{
|
||||
// FIXME: custom title components won't get an accessible title.
|
||||
// Do we really want to support them or shall we just limit this ModalTabsHeader?
|
||||
typeof title !== 'string' && title
|
||||
}
|
||||
<div className={styles.modalHeaderClose}>
|
||||
<IconButton aria-label="Close dialogue" surface="header" name="times" size="xl" onClick={onDismiss} />
|
||||
</div>
|
||||
@@ -130,11 +124,12 @@ function ModalButtonRow({ leftItems, children }: { leftItems?: React.ReactNode;
|
||||
Modal.ButtonRow = ModalButtonRow;
|
||||
|
||||
interface DefaultModalHeaderProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
icon?: IconName;
|
||||
iconTooltip?: string;
|
||||
}
|
||||
|
||||
function DefaultModalHeader({ icon, iconTooltip, title }: DefaultModalHeaderProps): JSX.Element {
|
||||
return <ModalHeader icon={icon} iconTooltip={iconTooltip} title={title} />;
|
||||
function DefaultModalHeader({ icon, iconTooltip, title, id }: DefaultModalHeaderProps): JSX.Element {
|
||||
return <ModalHeader icon={icon} iconTooltip={iconTooltip} title={title} id={id} />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useStyles2 } from '../../themes';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
id?: string;
|
||||
/** @deprecated */
|
||||
icon?: IconName;
|
||||
/** @deprecated */
|
||||
@@ -12,12 +13,14 @@ interface Props {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const ModalHeader: React.FC<Props> = ({ icon, iconTooltip, title, children }) => {
|
||||
export const ModalHeader: React.FC<Props> = ({ icon, iconTooltip, title, children, id }) => {
|
||||
const styles = useStyles2(getModalStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className={styles.modalHeaderTitle}>{title}</h2>
|
||||
<h2 className={styles.modalHeaderTitle} id={id}>
|
||||
{title}
|
||||
</h2>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
+11
-10
@@ -11,6 +11,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -21,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -132,7 +133,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) response.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
hashedState := hashStatecode(state, provider.ClientSecret)
|
||||
hashedState := hs.hashStatecode(state, provider.ClientSecret)
|
||||
cookies.WriteCookie(ctx.Resp, OauthStateCookieName, hashedState, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
|
||||
if provider.HostedDomain != "" {
|
||||
opts = append(opts, oauth2.SetAuthURLParam("hd", provider.HostedDomain))
|
||||
@@ -155,7 +156,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) response.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
queryState := hashStatecode(ctx.Query("state"), provider.ClientSecret)
|
||||
queryState := hs.hashStatecode(ctx.Query("state"), provider.ClientSecret)
|
||||
oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
|
||||
if cookieState != queryState {
|
||||
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
|
||||
@@ -234,7 +235,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) response.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
loginInfo.ExternalUser = *buildExternalUserInfo(token, userInfo, name)
|
||||
loginInfo.ExternalUser = *hs.buildExternalUserInfo(token, userInfo, name)
|
||||
loginInfo.User, err = syncUser(ctx, &loginInfo.ExternalUser, connect)
|
||||
if err != nil {
|
||||
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, err)
|
||||
@@ -265,7 +266,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
// buildExternalUserInfo returns a ExternalUserInfo struct from OAuth user profile
|
||||
func buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *models.ExternalUserInfo {
|
||||
func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *models.ExternalUserInfo {
|
||||
oauthLogger.Debug("Building external user info from OAuth user info")
|
||||
|
||||
extUser := &models.ExternalUserInfo{
|
||||
@@ -279,13 +280,13 @@ func buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo,
|
||||
Groups: userInfo.Groups,
|
||||
}
|
||||
|
||||
if userInfo.Role != "" {
|
||||
if userInfo.Role != "" && !hs.Cfg.OAuthSkipOrgRoleUpdateSync {
|
||||
rt := models.RoleType(userInfo.Role)
|
||||
if rt.IsValid() {
|
||||
// The user will be assigned a role in either the auto-assigned organization or in the default one
|
||||
var orgID int64
|
||||
if setting.AutoAssignOrg && setting.AutoAssignOrgId > 0 {
|
||||
orgID = int64(setting.AutoAssignOrgId)
|
||||
if hs.Cfg.AutoAssignOrg && hs.Cfg.AutoAssignOrgId > 0 {
|
||||
orgID = int64(hs.Cfg.AutoAssignOrgId)
|
||||
plog.Debug("The user has a role assignment and organization membership is auto-assigned",
|
||||
"role", userInfo.Role, "orgId", orgID)
|
||||
} else {
|
||||
@@ -327,8 +328,8 @@ func syncUser(
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func hashStatecode(code, seed string) string {
|
||||
hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
|
||||
func (hs *HTTPServer) hashStatecode(code, seed string) string {
|
||||
hashBytes := sha256.Sum256([]byte(code + hs.Cfg.SecretKey + seed))
|
||||
return hex.EncodeToString(hashBytes[:])
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package secretsmigrations
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -17,69 +17,123 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
|
||||
func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
Secret string
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
|
||||
return
|
||||
}
|
||||
|
||||
var anyFailure bool
|
||||
|
||||
for _, row := range rows {
|
||||
if len(row.Secret) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
decoded = []byte(row.Secret)
|
||||
)
|
||||
|
||||
if s.isBase64Encoded {
|
||||
decoded, err = base64.StdEncoding.DecodeString(row.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), row.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encrypted, err := secretsSrv.EncryptWithDBSession(context.Background(), decrypted, secrets.WithoutScope(), sess)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not encrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded := string(encrypted)
|
||||
if s.isBase64Encoded {
|
||||
encoded = base64.StdEncoding.EncodeToString(encrypted)
|
||||
}
|
||||
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
if _, err := sess.Exec(updateSQL, encoded, row.Id); err != nil {
|
||||
return err
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ?, updated = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
if _, err = sess.Exec(updateSQL, encrypted, nowInUTC(), row.Id); err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Column %s from %s has been re-encrypted successfully\n", s.columnName, s.tableName)
|
||||
|
||||
return nil
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Column %s from %s has been re-encrypted with errors", s.columnName, s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
|
||||
func (s b64Secret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
Secret string
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
|
||||
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
|
||||
return
|
||||
}
|
||||
|
||||
var anyFailure bool
|
||||
|
||||
for _, row := range rows {
|
||||
if len(row.Secret) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(row.Secret)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decode base64-encoded secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encrypted, err := secretsSrv.EncryptWithDBSession(context.Background(), decrypted, secrets.WithoutScope(), sess)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not encrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
_, err = sess.Exec(updateSQL, encoded, row.Id)
|
||||
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Column %s from %s has been re-encrypted with errors", s.columnName, s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
SecureJsonData map[string][]byte
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
|
||||
return
|
||||
}
|
||||
|
||||
var anyFailure bool
|
||||
|
||||
for _, row := range rows {
|
||||
if len(row.SecureJsonData) == 0 {
|
||||
continue
|
||||
@@ -87,29 +141,38 @@ func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Ses
|
||||
|
||||
decrypted, err := secretsSrv.DecryptJsonData(context.Background(), row.SecureJsonData)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secrets while re-encrypting them", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var toUpdate struct {
|
||||
toUpdate := struct {
|
||||
SecureJsonData map[string][]byte
|
||||
}
|
||||
Updated string
|
||||
}{Updated: nowInUTC()}
|
||||
|
||||
toUpdate.SecureJsonData, err = secretsSrv.EncryptJsonDataWithDBSession(context.Background(), decrypted, secrets.WithoutScope(), sess)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not re-encrypt secrets", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := sess.Table(s.tableName).Where("id = ?", row.Id).Update(toUpdate); err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secrets while re-encrypting them", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Secure json data from %s has been re-encrypted successfully\n", s.tableName)
|
||||
|
||||
return nil
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted with errors", s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted successfully", s.tableName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
|
||||
func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) {
|
||||
var results []struct {
|
||||
Id int
|
||||
AlertmanagerConfiguration string
|
||||
@@ -117,14 +180,19 @@ func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm
|
||||
|
||||
selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration"
|
||||
if err := sess.SQL(selectSQL).Find(&results); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any alert_configuration secret to re-encrypt")
|
||||
return
|
||||
}
|
||||
|
||||
var anyFailure bool
|
||||
|
||||
for _, result := range results {
|
||||
result := result
|
||||
postableUserConfig, err := notifier.Load([]byte(result.AlertmanagerConfiguration))
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not load alert_configuration while re-encrypting it", "id", result.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, receiver := range postableUserConfig.AlertmanagerConfig.Receivers {
|
||||
@@ -132,17 +200,23 @@ func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm
|
||||
for k, v := range gmr.SecureSettings {
|
||||
decoded, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decode base64-encoded alert_configuration secret", "id", result.Id, "key", k, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt alert_configuration secret", "id", result.Id, "key", k, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
reencrypted, err := secretsSrv.EncryptWithDBSession(context.Background(), decrypted, secrets.WithoutScope(), sess)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not re-encrypt alert_configuration secret", "id", result.Id, "key", k, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
gmr.SecureSettings[k] = base64.StdEncoding.EncodeToString(reencrypted)
|
||||
@@ -152,18 +226,24 @@ func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm
|
||||
|
||||
marshalled, err := json.Marshal(postableUserConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not marshal alert_configuration while re-encrypting it", "id", result.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
result.AlertmanagerConfiguration = string(marshalled)
|
||||
if _, err := sess.Table("alert_configuration").Where("id = ?", result.Id).Update(&result); err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update alert_configuration secret while re-encrypting it", "id", result.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Alerting secrets has been re-encrypted successfully\n")
|
||||
|
||||
return nil
|
||||
if anyFailure {
|
||||
logger.Warn("Alerting configuration secrets have been re-encrypted with errors")
|
||||
} else {
|
||||
logger.Info("Alerting configuration secrets have been re-encrypted successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
||||
@@ -173,22 +253,27 @@ func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
||||
}
|
||||
|
||||
toMigrate := []interface {
|
||||
reencrypt(*manager.SecretsService, *xorm.Session) error
|
||||
reencrypt(*manager.SecretsService, *xorm.Session)
|
||||
}{
|
||||
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted", isBase64Encoded: false},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted"},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token"}},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type"}},
|
||||
jsonSecret{tableName: "data_source"},
|
||||
jsonSecret{tableName: "plugin_setting"},
|
||||
alertingSecret{},
|
||||
}
|
||||
|
||||
return runner.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
for _, m := range toMigrate {
|
||||
if err := m.reencrypt(runner.SecretsService, sess.Session); err != nil {
|
||||
return err
|
||||
return runner.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New(fmt.Sprint(r))
|
||||
logger.Error("Secrets re-encryption failed, rolling back transaction...", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, m := range toMigrate {
|
||||
m.reencrypt(runner.SecretsService, sess.Session)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/services/encryption"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -23,14 +22,15 @@ func (s simpleSecret) rollback(
|
||||
encryptionSrv encryption.Internal,
|
||||
sess *xorm.Session,
|
||||
secretKey string,
|
||||
) error {
|
||||
) (anyFailure bool) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
Secret string
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any secret to roll back", "table", s.tableName)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
@@ -38,42 +38,95 @@ func (s simpleSecret) rollback(
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
decoded = []byte(row.Secret)
|
||||
)
|
||||
|
||||
if s.isBase64Encoded {
|
||||
decoded, err = base64.StdEncoding.DecodeString(row.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), row.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not encrypt secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded := string(encrypted)
|
||||
if s.isBase64Encoded {
|
||||
encoded = base64.StdEncoding.EncodeToString(encrypted)
|
||||
}
|
||||
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
if _, err := sess.Exec(updateSQL, encoded, row.Id); err != nil {
|
||||
return err
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ?, updated = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
if _, err = sess.Exec(updateSQL, encrypted, nowInUTC(), row.Id); err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Column %s from %s have been rolled back successfully\n", s.columnName, s.tableName)
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Column %s from %s has been rolled back with errors", s.columnName, s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Column %s from %s has been rolled back successfully", s.columnName, s.tableName))
|
||||
}
|
||||
|
||||
return nil
|
||||
return anyFailure
|
||||
}
|
||||
|
||||
func (s b64Secret) rollback(
|
||||
secretsSrv *manager.SecretsService,
|
||||
encryptionSrv encryption.Internal,
|
||||
sess *xorm.Session,
|
||||
secretKey string,
|
||||
) (anyFailure bool) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
Secret string
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
|
||||
logger.Warn("Could not find any secret to roll back", "table", s.tableName)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if len(row.Secret) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(row.Secret)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decode base64-encoded secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey)
|
||||
if err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not encrypt secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
|
||||
if _, err := sess.Exec(updateSQL, encoded, row.Id); err != nil {
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secret while rolling it back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Column %s from %s has been rolled back with errors", s.columnName, s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Column %s from %s has been rolled back successfully", s.columnName, s.tableName))
|
||||
}
|
||||
|
||||
return anyFailure
|
||||
}
|
||||
|
||||
func (s jsonSecret) rollback(
|
||||
@@ -81,14 +134,15 @@ func (s jsonSecret) rollback(
|
||||
encryptionSrv encryption.Internal,
|
||||
sess *xorm.Session,
|
||||
secretKey string,
|
||||
) error {
|
||||
) (anyFailure bool) {
|
||||
var rows []struct {
|
||||
Id int
|
||||
SecureJsonData map[string][]byte
|
||||
}
|
||||
|
||||
if err := sess.Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any secret to roll back", "table", s.tableName)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
@@ -98,26 +152,35 @@ func (s jsonSecret) rollback(
|
||||
|
||||
decrypted, err := secretsSrv.DecryptJsonData(context.Background(), row.SecureJsonData)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secrets while rolling them back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var toUpdate struct {
|
||||
toUpdate := struct {
|
||||
SecureJsonData map[string][]byte
|
||||
}
|
||||
Updated string
|
||||
}{Updated: nowInUTC()}
|
||||
|
||||
toUpdate.SecureJsonData, err = encryptionSrv.EncryptJsonData(context.Background(), decrypted, secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
logger.Warn("Could not re-encrypt secrets while rolling them back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := sess.Table(s.tableName).Where("id = ?", row.Id).Update(toUpdate); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not update secrets while rolling them back", "table", s.tableName, "id", row.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Secure json data from %s have been rolled back successfully\n", s.tableName)
|
||||
if anyFailure {
|
||||
logger.Warn(fmt.Sprintf("Secure json data secrets from %s have been rolled back with errors", s.tableName))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Secure json data secrets from %s have been rolled back successfully", s.tableName))
|
||||
}
|
||||
|
||||
return nil
|
||||
return anyFailure
|
||||
}
|
||||
|
||||
func (s alertingSecret) rollback(
|
||||
@@ -125,7 +188,7 @@ func (s alertingSecret) rollback(
|
||||
encryptionSrv encryption.Internal,
|
||||
sess *xorm.Session,
|
||||
secretKey string,
|
||||
) error {
|
||||
) (anyFailure bool) {
|
||||
var results []struct {
|
||||
Id int
|
||||
AlertmanagerConfiguration string
|
||||
@@ -133,14 +196,17 @@ func (s alertingSecret) rollback(
|
||||
|
||||
selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration"
|
||||
if err := sess.SQL(selectSQL).Find(&results); err != nil {
|
||||
return err
|
||||
logger.Warn("Could not find any alert_configuration secret to roll back")
|
||||
return true
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
result := result
|
||||
postableUserConfig, err := notifier.Load([]byte(result.AlertmanagerConfiguration))
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not load configuration (alert_configuration with id: %d) while rolling it back", result.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, receiver := range postableUserConfig.AlertmanagerConfig.Receivers {
|
||||
@@ -148,17 +214,23 @@ func (s alertingSecret) rollback(
|
||||
for k, v := range gmr.SecureSettings {
|
||||
decoded, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decode base64-encoded secret (alert_configuration with id: %d, key)", k, result.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not decrypt secret (alert_configuration with id: %d, key)", k, result.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
reencrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not re-encrypt secret (alert_configuration with id: %d, key)", k, result.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
gmr.SecureSettings[k] = base64.StdEncoding.EncodeToString(reencrypted)
|
||||
@@ -168,18 +240,26 @@ func (s alertingSecret) rollback(
|
||||
|
||||
marshalled, err := json.Marshal(postableUserConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not marshal configuration (alert_configuration with id: %d) while rolling it back", result.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
result.AlertmanagerConfiguration = string(marshalled)
|
||||
if _, err := sess.Table("alert_configuration").Where("id = ?", result.Id).Update(&result); err != nil {
|
||||
return err
|
||||
anyFailure = true
|
||||
logger.Warn("Could not update secret (alert_configuration with id: %d) while rolling it back", result.Id, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Alerting secrets have rolled re-encrypted successfully\n")
|
||||
if anyFailure {
|
||||
logger.Warn("Alerting configuration secrets have been rolled back with errors")
|
||||
} else {
|
||||
logger.Info("Alerting configuration secrets have been rolled back successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
return anyFailure
|
||||
}
|
||||
|
||||
func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
||||
@@ -188,31 +268,41 @@ func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
toMigrate := []interface {
|
||||
rollback(*manager.SecretsService, encryption.Internal, *xorm.Session, string) error
|
||||
toRollback := []interface {
|
||||
rollback(*manager.SecretsService, encryption.Internal, *xorm.Session, string) bool
|
||||
}{
|
||||
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted", isBase64Encoded: false},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type", isBase64Encoded: true},
|
||||
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted"},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token"}},
|
||||
b64Secret{simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type"}},
|
||||
jsonSecret{tableName: "data_source"},
|
||||
jsonSecret{tableName: "plugin_setting"},
|
||||
alertingSecret{},
|
||||
}
|
||||
|
||||
return runner.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
for _, m := range toMigrate {
|
||||
if err := m.rollback(
|
||||
runner.SecretsService,
|
||||
runner.EncryptionService,
|
||||
sess.Session,
|
||||
runner.Cfg.SecretKey); err != nil {
|
||||
return err
|
||||
return runner.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New(fmt.Sprint(r))
|
||||
logger.Error("Secrets roll back failed, rolling back transaction...", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var anyFailure bool
|
||||
|
||||
for _, r := range toRollback {
|
||||
if failed := r.rollback(runner.SecretsService, runner.EncryptionService, sess.Session, runner.Cfg.SecretKey); failed {
|
||||
anyFailure = true
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM data_keys"); err != nil {
|
||||
logger.Warn("Error while cleaning up data keys table...", "err", err)
|
||||
if anyFailure {
|
||||
logger.Warn("Some errors happened, not cleaning up data keys table...")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, sqlErr := sess.Exec("DELETE FROM data_keys"); sqlErr != nil {
|
||||
logger.Warn("Error while cleaning up data keys table...", "error", sqlErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package secretsmigrations
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
type simpleSecret struct {
|
||||
tableName string
|
||||
columnName string
|
||||
}
|
||||
|
||||
type b64Secret struct {
|
||||
simpleSecret
|
||||
}
|
||||
|
||||
type jsonSecret struct {
|
||||
tableName string
|
||||
}
|
||||
|
||||
type alertingSecret struct{}
|
||||
|
||||
func nowInUTC() string {
|
||||
return time.Now().UTC().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
var logger = log.New("secrets.migrations")
|
||||
@@ -1,13 +0,0 @@
|
||||
package secretsmigrations
|
||||
|
||||
type simpleSecret struct {
|
||||
tableName string
|
||||
columnName string
|
||||
isBase64Encoded bool
|
||||
}
|
||||
|
||||
type jsonSecret struct {
|
||||
tableName string
|
||||
}
|
||||
|
||||
type alertingSecret struct{}
|
||||
@@ -17,3 +17,11 @@ const (
|
||||
type Service interface {
|
||||
Provide() (map[secrets.ProviderID]secrets.Provider, error)
|
||||
}
|
||||
|
||||
func NormalizeProviderID(id secrets.ProviderID) secrets.ProviderID {
|
||||
if id == Legacy {
|
||||
return Default
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"xorm.io/xorm"
|
||||
@@ -100,26 +101,50 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys(
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
provider, ok := providers[k.Provider]
|
||||
provider, ok := providers[kmsproviders.NormalizeProviderID(k.Provider)]
|
||||
if !ok {
|
||||
return fmt.Errorf("could not find encryption provider '%s'", k.Provider)
|
||||
ss.log.Warn(
|
||||
"Could not find provider to re-encrypt data encryption key",
|
||||
"key_id", k.Name,
|
||||
"provider", k.Provider,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
decrypted, err := provider.Decrypt(ctx, k.EncryptedData)
|
||||
if err != nil {
|
||||
return err
|
||||
ss.log.Warn(
|
||||
"Error while decrypting data encryption key to re-encrypt it",
|
||||
"key_id", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Updating current data key by re-encrypting it with current provider.
|
||||
// Accessing the current provider within providers map should be safe.
|
||||
k.Provider = currProvider
|
||||
k.Updated = time.Now()
|
||||
k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
ss.log.Warn(
|
||||
"Error while re-encrypting data encryption key",
|
||||
"key_id", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Name).Update(k); err != nil {
|
||||
return err
|
||||
ss.log.Warn(
|
||||
"Error while re-encrypting data encryption key",
|
||||
"key_id", k.Name,
|
||||
"provider", k.Provider,
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func ProvideSecretsService(
|
||||
|
||||
logger := log.New("secrets")
|
||||
enabled := features.IsEnabled(featuremgmt.FlagEnvelopeEncryption)
|
||||
currentProviderID := normalizeProviderID(secrets.ProviderID(
|
||||
currentProviderID := kmsproviders.NormalizeProviderID(secrets.ProviderID(
|
||||
settings.KeyValue("security", "encryption_provider").MustString(kmsproviders.Default),
|
||||
))
|
||||
|
||||
@@ -79,14 +79,6 @@ func ProvideSecretsService(
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func normalizeProviderID(id secrets.ProviderID) secrets.ProviderID {
|
||||
if id == kmsproviders.Legacy {
|
||||
return kmsproviders.Default
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *SecretsService) registerUsageMetrics() {
|
||||
s.usageStats.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
|
||||
usageMetrics := make(map[string]interface{})
|
||||
@@ -330,7 +322,7 @@ func (s *SecretsService) dataKey(ctx context.Context, name string) ([]byte, erro
|
||||
}
|
||||
|
||||
// 2. decrypt data key
|
||||
provider, exists := s.providers[normalizeProviderID(dataKey.Provider)]
|
||||
provider, exists := s.providers[kmsproviders.NormalizeProviderID(dataKey.Provider)]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,11 @@ func TestMigrationLock(t *testing.T) {
|
||||
func TestMigratorLocking(t *testing.T) {
|
||||
dbType := getDBType()
|
||||
testDB := getTestDB(t, dbType)
|
||||
// skip for SQLite for now since it occasionally fails for not clear reason
|
||||
// anyway starting migrations concurretly for the same migrator is impossible use case
|
||||
if dbType == SQLite {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
|
||||
require.NoError(t, err)
|
||||
@@ -181,7 +186,7 @@ func TestMigratorLocking(t *testing.T) {
|
||||
})
|
||||
}
|
||||
})
|
||||
assert.Equal(t, int64(1), errorNum)
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&errorNum))
|
||||
}
|
||||
|
||||
func TestDatabaseLocking(t *testing.T) {
|
||||
|
||||
@@ -115,15 +115,31 @@ func (b *Builder) applyFilters() (ordering string) {
|
||||
b.params = append(b.params, whereParams...)
|
||||
}
|
||||
|
||||
if len(groups) > 0 {
|
||||
b.sql.WriteString(fmt.Sprintf(" GROUP BY %s", strings.Join(groups, ", ")))
|
||||
b.params = append(b.params, groupParams...)
|
||||
}
|
||||
|
||||
if len(orders) < 1 {
|
||||
orders = append(orders, TitleSorter{}.OrderBy())
|
||||
}
|
||||
|
||||
if len(groups) > 0 {
|
||||
cols := make([]string, 0, len(orders)+len(groups))
|
||||
for _, o := range orders {
|
||||
o := strings.TrimSuffix(o, " DESC")
|
||||
o = strings.TrimSuffix(o, " ASC")
|
||||
exists := false
|
||||
for _, g := range groups {
|
||||
if g == o {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
cols = append(cols, o)
|
||||
}
|
||||
}
|
||||
cols = append(cols, groups...)
|
||||
b.sql.WriteString(fmt.Sprintf(" GROUP BY %s", strings.Join(cols, ", ")))
|
||||
b.params = append(b.params, groupParams...)
|
||||
}
|
||||
|
||||
orderBy := fmt.Sprintf(" ORDER BY %s", strings.Join(orders, ", "))
|
||||
b.sql.WriteString(orderBy)
|
||||
|
||||
|
||||
@@ -393,9 +393,10 @@ type Cfg struct {
|
||||
DefaultTheme string
|
||||
HomePage string
|
||||
|
||||
AutoAssignOrg bool
|
||||
AutoAssignOrgId int
|
||||
AutoAssignOrgRole string
|
||||
AutoAssignOrg bool
|
||||
AutoAssignOrgId int
|
||||
AutoAssignOrgRole string
|
||||
OAuthSkipOrgRoleUpdateSync bool
|
||||
|
||||
// ExpressionsEnabled specifies whether expressions are enabled.
|
||||
ExpressionsEnabled bool
|
||||
@@ -1256,6 +1257,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
|
||||
cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600)
|
||||
SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "")
|
||||
cfg.OAuthSkipOrgRoleUpdateSync = auth.Key("oauth_skip_org_role_update_sync").MustBool(false)
|
||||
|
||||
// SigV4
|
||||
SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"timeSeries": [
|
||||
{
|
||||
"metric": {
|
||||
"type": "loadbalancing.googleapis.com\/https\/backend_latencies"
|
||||
},
|
||||
"resource": {
|
||||
"type": "https_lb_rule",
|
||||
"labels": {
|
||||
"project_id": "grafana-prod"
|
||||
}
|
||||
},
|
||||
"metricKind": "DELTA",
|
||||
"valueType": "DISTRIBUTION"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -166,11 +166,10 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseResponse(queryRes
|
||||
if len(point.Value.DistributionValue.BucketCounts) == 0 {
|
||||
continue
|
||||
}
|
||||
maxKey := 0
|
||||
for i := 0; i < len(point.Value.DistributionValue.BucketCounts); i++ {
|
||||
value, err := strconv.ParseFloat(point.Value.DistributionValue.BucketCounts[i], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
return err
|
||||
}
|
||||
if _, ok := buckets[i]; !ok {
|
||||
// set lower bounds
|
||||
@@ -193,40 +192,21 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseResponse(queryRes
|
||||
valueField,
|
||||
},
|
||||
RefID: timeSeriesFilter.RefID,
|
||||
}
|
||||
|
||||
if maxKey < i {
|
||||
maxKey = i
|
||||
Meta: &data.FrameMeta{
|
||||
ExecutedQueryString: executedQueryString,
|
||||
},
|
||||
}
|
||||
}
|
||||
buckets[i].AppendRow(point.Interval.EndTime, value)
|
||||
}
|
||||
for i := 0; i < maxKey; i++ {
|
||||
if _, ok := buckets[i]; !ok {
|
||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||
additionalLabels := data.Labels{"bucket": bucketBound}
|
||||
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, []time.Time{})
|
||||
valueField := data.NewField(data.TimeSeriesValueFieldName, nil, []float64{})
|
||||
frameName := formatLegendKeys(series.Metric.Type, defaultMetricName, seriesLabels,
|
||||
additionalLabels, timeSeriesFilter)
|
||||
valueField.Name = frameName
|
||||
valueField.Labels = seriesLabels
|
||||
setDisplayNameAsFieldName(valueField)
|
||||
|
||||
buckets[i] = &data.Frame{
|
||||
Name: frameName,
|
||||
RefID: timeSeriesFilter.RefID,
|
||||
Fields: []*data.Field{
|
||||
timeField,
|
||||
valueField,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(buckets); i++ {
|
||||
buckets[i].Meta.Custom = customFrameMeta
|
||||
frames = append(frames, buckets[i])
|
||||
}
|
||||
if len(buckets) == 0 {
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
}
|
||||
if len(response.TimeSeries) > 0 {
|
||||
dl := timeSeriesFilter.buildDeepLink()
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
sdkdata "github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -386,6 +388,77 @@ func TestTimeSeriesFilter(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "114250375703598695", labels["resource.label.instance_id"])
|
||||
})
|
||||
|
||||
t.Run("parseResponse successfully parses metadata for distribution valueType", func(t *testing.T) {
|
||||
t.Run("exponential bounds", func(t *testing.T) {
|
||||
data, err := loadTestFile("./test-data/3-series-response-distribution-exponential.json")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(data.TimeSeries))
|
||||
|
||||
res := &backend.DataResponse{}
|
||||
require.NoError(t, (&cloudMonitoringTimeSeriesFilter{GroupBys: []string{"test_group_by"}}).parseResponse(res, data, "test_query"))
|
||||
|
||||
require.NotNil(t, res.Frames[0].Meta)
|
||||
assert.Equal(t, sdkdata.FrameMeta{
|
||||
ExecutedQueryString: "test_query",
|
||||
Custom: map[string]interface{}{
|
||||
"groupBys": []string{"test_group_by"},
|
||||
"alignmentPeriod": "",
|
||||
"labels": map[string]string{
|
||||
"resource.label.project_id": "grafana-prod",
|
||||
"resource.type": "https_lb_rule",
|
||||
},
|
||||
"perSeriesAligner": "",
|
||||
},
|
||||
}, *res.Frames[0].Meta)
|
||||
})
|
||||
|
||||
t.Run("explicit bounds", func(t *testing.T) {
|
||||
data, err := loadTestFile("./test-data/4-series-response-distribution-explicit.json")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(data.TimeSeries))
|
||||
|
||||
res := &backend.DataResponse{}
|
||||
require.NoError(t, (&cloudMonitoringTimeSeriesFilter{GroupBys: []string{"test_group_by"}}).parseResponse(res, data, "test_query"))
|
||||
|
||||
require.NotNil(t, res.Frames[0].Meta)
|
||||
assert.Equal(t, sdkdata.FrameMeta{
|
||||
ExecutedQueryString: "test_query",
|
||||
Custom: map[string]interface{}{
|
||||
"groupBys": []string{"test_group_by"},
|
||||
"alignmentPeriod": "",
|
||||
"labels": map[string]string{
|
||||
"resource.label.project_id": "grafana-demo",
|
||||
"resource.type": "global",
|
||||
},
|
||||
"perSeriesAligner": "",
|
||||
},
|
||||
}, *res.Frames[0].Meta)
|
||||
})
|
||||
|
||||
t.Run("without series points", func(t *testing.T) {
|
||||
data, err := loadTestFile("./test-data/3-series-response-distribution-exponential.json")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(data.TimeSeries))
|
||||
|
||||
res := &backend.DataResponse{}
|
||||
require.NoError(t, (&cloudMonitoringTimeSeriesFilter{GroupBys: []string{"test_group_by"}}).parseResponse(res, data, "test_query"))
|
||||
|
||||
require.NotNil(t, res.Frames[0].Meta)
|
||||
assert.Equal(t, sdkdata.FrameMeta{
|
||||
ExecutedQueryString: "test_query",
|
||||
Custom: map[string]interface{}{
|
||||
"groupBys": []string{"test_group_by"},
|
||||
"alignmentPeriod": "",
|
||||
"labels": map[string]string{
|
||||
"resource.label.project_id": "grafana-prod",
|
||||
"resource.type": "https_lb_rule",
|
||||
},
|
||||
"perSeriesAligner": "",
|
||||
},
|
||||
}, *res.Frames[0].Meta)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loadTestFile(path string) (cloudMonitoringResponse, error) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme, KeyValue } from '@grafana/data';
|
||||
import { SegmentInput, useStyles, InlineLabel, Icon } from '@grafana/ui';
|
||||
import React from 'react';
|
||||
|
||||
const EQ_WIDTH = 3; // = 24px in inline label
|
||||
|
||||
interface Props {
|
||||
values: Array<KeyValue<string>>;
|
||||
onChange: (values: Array<KeyValue<string>>) => void;
|
||||
id?: string;
|
||||
keyPlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
}
|
||||
|
||||
const KeyValueInput = ({
|
||||
values,
|
||||
onChange,
|
||||
id,
|
||||
keyPlaceholder = 'Key',
|
||||
valuePlaceholder = 'Value (optional)',
|
||||
}: Props) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{values.length ? (
|
||||
values.map((value, idx) => (
|
||||
<div className={styles.pair} key={idx}>
|
||||
<SegmentInput
|
||||
id={`${id}-key-${idx}`}
|
||||
placeholder={keyPlaceholder}
|
||||
value={value.key}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
values.map((v, i) => {
|
||||
if (i === idx) {
|
||||
v.key = String(e);
|
||||
}
|
||||
return v;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<InlineLabel aria-label="equals" className={styles.operator} width={EQ_WIDTH}>
|
||||
=
|
||||
</InlineLabel>
|
||||
<SegmentInput
|
||||
id={`${id}-value-${idx}`}
|
||||
placeholder={valuePlaceholder}
|
||||
value={value.value}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
values.map((v, i) => {
|
||||
if (i === idx) {
|
||||
v.value = String(e);
|
||||
}
|
||||
return v;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => onChange([...values.slice(0, idx), ...values.slice(idx + 1)])}
|
||||
className="gf-form-label query-part"
|
||||
aria-label="Remove tag"
|
||||
>
|
||||
<Icon name="times" />
|
||||
</button>
|
||||
{idx === values.length - 1 ? (
|
||||
<button
|
||||
onClick={() => onChange([...values, { key: '', value: '' }])}
|
||||
className="gf-form-label query-part"
|
||||
aria-label="Add tag"
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onChange([...values, { key: '', value: '' }])}
|
||||
className="gf-form-label query-part"
|
||||
aria-label="Add tag"
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyValueInput;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing.xs} 0;
|
||||
`,
|
||||
pair: css`
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
`,
|
||||
operator: css`
|
||||
color: ${theme.palette.orange};
|
||||
`,
|
||||
});
|
||||
+58
-8
@@ -3,15 +3,19 @@ import {
|
||||
DataSourceJsonData,
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
GrafanaTheme,
|
||||
KeyValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
} from '@grafana/data';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, Input, TagsInput, useStyles, InlineSwitch } from '@grafana/ui';
|
||||
import React from 'react';
|
||||
import KeyValueInput from './KeyValueInput';
|
||||
|
||||
export interface TraceToLogsOptions {
|
||||
datasourceUid?: string;
|
||||
tags?: string[];
|
||||
mappedTags?: Array<KeyValue<string>>;
|
||||
mapTagNamesEnabled?: boolean;
|
||||
spanStartTimeShift?: string;
|
||||
spanEndTimeShift?: string;
|
||||
filterByTraceID?: boolean;
|
||||
@@ -54,19 +58,64 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
{options.jsonData.tracesToLogs?.mapTagNamesEnabled ? (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
labelWidth={26}
|
||||
>
|
||||
<KeyValueInput
|
||||
keyPlaceholder="Tag"
|
||||
values={
|
||||
options.jsonData.tracesToLogs?.mappedTags ??
|
||||
options.jsonData.tracesToLogs?.tags?.map((tag) => ({ key: tag })) ??
|
||||
[]
|
||||
}
|
||||
onChange={(v) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
mappedTags: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
) : (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
labelWidth={26}
|
||||
>
|
||||
<TagsInput
|
||||
tags={options.jsonData.tracesToLogs?.tags}
|
||||
width={40}
|
||||
onChange={(tags) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
tags: tags,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
label="Map tag names"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Map trace tag names to log label names. Ex: k8s.pod.name -> pod"
|
||||
>
|
||||
<TagsInput
|
||||
tags={options.jsonData.tracesToLogs?.tags}
|
||||
width={40}
|
||||
onChange={(tags) =>
|
||||
<InlineSwitch
|
||||
id="mapTagNames"
|
||||
value={options.jsonData.tracesToLogs?.mapTagNamesEnabled ?? false}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
datasourceUid: options.jsonData.tracesToLogs?.datasourceUid,
|
||||
tags: tags,
|
||||
...options.jsonData.tracesToLogs,
|
||||
mapTagNamesEnabled: event.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -156,6 +205,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Loki Search" labelWidth={26} grow tooltip="Use this logs data source to search for traces.">
|
||||
<InlineSwitch
|
||||
@@ -105,7 +105,12 @@ export async function parseResponseBody<T>(
|
||||
return response.blob() as any;
|
||||
|
||||
case 'json':
|
||||
return response.json();
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.warn(`${response.url} returned an invalid JSON -`, err);
|
||||
return {} as unknown as T;
|
||||
}
|
||||
|
||||
case 'text':
|
||||
return response.text() as any;
|
||||
|
||||
@@ -81,8 +81,8 @@ function LogsNavigation({
|
||||
}, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearCache();
|
||||
// We can't enforce the eslint rule here because we only want to run when component unmounts.
|
||||
clearCache();
|
||||
// We can't enforce the eslint rule here because we only want to run when component is mounted.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
TTraceTimeline,
|
||||
UIElementsContext,
|
||||
} from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
|
||||
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from '../../../core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
@@ -141,6 +141,60 @@ describe('createSpanLinkFactory', () => {
|
||||
|
||||
expect(linkDef!.href).toBe('testSpanId');
|
||||
});
|
||||
|
||||
it('handles renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
{ key: 'service.name', value: 'service' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'serviceName' },
|
||||
{ key: 'k8s.pod.name', value: 'podName' },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles incomplete renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
{ key: 'service.name', value: '' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'serviceName' },
|
||||
{ key: 'k8s.pod.name', value: 'podName' },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DataLink,
|
||||
dateTime,
|
||||
Field,
|
||||
KeyValue,
|
||||
mapInternalLinkToExplore,
|
||||
rangeUtil,
|
||||
SplitOpen,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import React from 'react';
|
||||
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
||||
@@ -122,11 +123,22 @@ function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?
|
||||
const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'];
|
||||
|
||||
function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): string {
|
||||
const { tags: keys, filterByTraceID, filterBySpanID } = options;
|
||||
const keysToCheck = keys?.length ? keys : defaultKeys;
|
||||
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
||||
|
||||
// In order, try to use mapped tags -> tags -> default tags
|
||||
const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
|
||||
|
||||
// Build tag portion of query
|
||||
const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
|
||||
if (keysToCheck.includes(tag.key)) {
|
||||
acc.push(`${tag.key}="${tag.value}"`);
|
||||
if (mapTagNamesEnabled) {
|
||||
const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
|
||||
if (keyValue) {
|
||||
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
|
||||
}
|
||||
} else {
|
||||
if ((keysToCheck as string[]).includes(tag.key)) {
|
||||
acc.push(`${tag.key}="${tag.value}"`);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
@@ -73,6 +73,8 @@ export const install = createAsyncThunk(
|
||||
|
||||
return { id, changes } as Update<CatalogPlugin>;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return thunkApi.rejectWithValue('Unknown error.');
|
||||
}
|
||||
}
|
||||
@@ -90,6 +92,8 @@ export const uninstall = createAsyncThunk(`${STATE_PREFIX}/uninstall`, async (id
|
||||
changes: { isInstalled: false, installedVersion: undefined },
|
||||
} as Update<CatalogPlugin>;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return thunkApi.rejectWithValue('Unknown error.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
||||
onChange={onMaxLimitChange}
|
||||
/>
|
||||
</Field>
|
||||
{type === GrafanaAnnotationType.Tags && tags && (
|
||||
{type === GrafanaAnnotationType.Tags && (
|
||||
<>
|
||||
<Field label="Match any" description={matchTooltipContent}>
|
||||
<Switch id="grafana-annotations__match-any" value={matchAny} onChange={onMatchAnyChange} />
|
||||
@@ -100,7 +100,7 @@ export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
||||
inputId="grafana-annotations__tags"
|
||||
onChange={onTagsChange}
|
||||
tagOptions={getAnnotationTags}
|
||||
tags={tags}
|
||||
tags={tags ?? []}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import React from 'react';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Themeable2,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import React from 'react';
|
||||
import { LokiQueryField } from '../../loki/components/LokiQueryField';
|
||||
import { LokiQuery } from '../../loki/types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import React from 'react';
|
||||
import { ServiceGraphSettings } from './ServiceGraphSettings';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
isValidGoDuration,
|
||||
LoadingState,
|
||||
} from '@grafana/data';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { config, BackendSrvRequest, DataSourceWithBackend, getBackendSrv } from '@grafana/runtime';
|
||||
import { serializeParams } from 'app/core/utils/fetch';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
|
||||
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import React from 'react';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
||||
@@ -110,7 +110,7 @@ RUN rm dockerize-linux-amd64-v${DOCKERIZE_VERSION}.tar.gz
|
||||
# Use old Debian (this has support into 2022) in order to ensure binary compatibility with older glibc's.
|
||||
FROM debian:stretch-20210208
|
||||
|
||||
ENV GOVERSION=1.17.6 \
|
||||
ENV GOVERSION=1.17.7 \
|
||||
PATH=/usr/local/go/bin:$PATH \
|
||||
GOPATH=/go \
|
||||
NODEVERSION=16.13.0-1nodesource1 \
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
load(
|
||||
'scripts/drone/steps/lib.star',
|
||||
'initialize_step',
|
||||
'download_grabpl_step',
|
||||
'lint_frontend_step',
|
||||
'codespell_step',
|
||||
'shellcheck_step',
|
||||
'build_frontend_step',
|
||||
'test_frontend_step',
|
||||
'build_storybook_step',
|
||||
'build_frontend_docs_step',
|
||||
'build_docs_website_step',
|
||||
)
|
||||
|
||||
load(
|
||||
'scripts/drone/services/services.star',
|
||||
'integration_test_services',
|
||||
'ldap_service',
|
||||
)
|
||||
|
||||
load(
|
||||
'scripts/drone/utils/utils.star',
|
||||
'pipeline',
|
||||
)
|
||||
|
||||
|
||||
def docs_pipelines(edition, ver_mode, trigger):
|
||||
steps = [download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode)
|
||||
|
||||
# Insert remaining steps
|
||||
steps.extend([
|
||||
build_frontend_docs_step(edition=edition),
|
||||
build_docs_website_step(),
|
||||
])
|
||||
|
||||
return pipeline(
|
||||
name='{}-docs'.format(ver_mode), edition=edition, trigger=trigger, services=[], steps=steps,
|
||||
)
|
||||
|
||||
|
||||
def trigger_docs():
|
||||
return {
|
||||
'event': [
|
||||
'pull_request',
|
||||
],
|
||||
'paths': {
|
||||
'include': [
|
||||
'docs/**',
|
||||
'packages/**',
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -18,7 +18,6 @@ load(
|
||||
'e2e_tests_step',
|
||||
'e2e_tests_artifacts',
|
||||
'build_storybook_step',
|
||||
'build_frontend_docs_step',
|
||||
'copy_packages_for_docker_step',
|
||||
'build_docker_images_step',
|
||||
'publish_images_step',
|
||||
@@ -55,6 +54,11 @@ load(
|
||||
'drone_change_template',
|
||||
)
|
||||
|
||||
load(
|
||||
'scripts/drone/pipelines/docs.star',
|
||||
'docs_pipelines',
|
||||
)
|
||||
|
||||
ver_mode = 'main'
|
||||
|
||||
def get_steps(edition, is_downstream=False):
|
||||
@@ -110,7 +114,6 @@ def get_steps(edition, is_downstream=False):
|
||||
store_storybook_step(edition=edition, ver_mode=ver_mode),
|
||||
test_a11y_frontend_step(ver_mode=ver_mode, edition=edition),
|
||||
frontend_metrics_step(edition=edition),
|
||||
build_frontend_docs_step(edition=edition),
|
||||
copy_packages_for_docker_step(),
|
||||
build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=False),
|
||||
build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=False),
|
||||
@@ -171,6 +174,7 @@ def main_pipelines(edition):
|
||||
integration_test_steps.append(benchmark_ldap_step())
|
||||
|
||||
pipelines = [
|
||||
docs_pipelines(edition, ver_mode, trigger),
|
||||
pipeline(
|
||||
name='main-test', edition=edition, trigger=trigger, services=[],
|
||||
steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + test_steps,
|
||||
|
||||
@@ -18,8 +18,6 @@ load(
|
||||
'e2e_tests_step',
|
||||
'e2e_tests_artifacts',
|
||||
'build_storybook_step',
|
||||
'build_frontend_docs_step',
|
||||
'build_docs_website_step',
|
||||
'copy_packages_for_docker_step',
|
||||
'build_docker_images_step',
|
||||
'postgres_integration_tests_step',
|
||||
@@ -47,6 +45,12 @@ load(
|
||||
'drone_change_template',
|
||||
)
|
||||
|
||||
load(
|
||||
'scripts/drone/pipelines/docs.star',
|
||||
'docs_pipelines',
|
||||
'trigger_docs',
|
||||
)
|
||||
|
||||
ver_mode = 'pr'
|
||||
|
||||
def pr_pipelines(edition):
|
||||
@@ -100,8 +104,6 @@ def pr_pipelines(edition):
|
||||
e2e_tests_artifacts(edition=edition),
|
||||
build_storybook_step(edition=edition, ver_mode=ver_mode),
|
||||
test_a11y_frontend_step(ver_mode=ver_mode, edition=edition),
|
||||
build_frontend_docs_step(edition=edition),
|
||||
build_docs_website_step(),
|
||||
copy_packages_for_docker_step(),
|
||||
build_docker_images_step(edition=edition, ver_mode=ver_mode, archs=['amd64',]),
|
||||
])
|
||||
@@ -116,7 +118,14 @@ def pr_pipelines(edition):
|
||||
])
|
||||
|
||||
trigger = {
|
||||
'event': ['pull_request',],
|
||||
'event': [
|
||||
'pull_request',
|
||||
],
|
||||
'paths': {
|
||||
'exclude': [
|
||||
'docs/**',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -130,5 +139,5 @@ def pr_pipelines(edition):
|
||||
name='pr-integration-tests', edition=edition, trigger=trigger, services=services,
|
||||
steps=[download_grabpl_step()] + integration_test_steps,
|
||||
volumes=volumes,
|
||||
),
|
||||
), docs_pipelines(edition, ver_mode, trigger_docs())
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket')
|
||||
|
||||
grabpl_version = 'v2.9.2'
|
||||
build_image = 'grafana/build-container:1.4.9'
|
||||
grabpl_version = 'v2.9.5'
|
||||
build_image = 'grafana/build-container:1.5.1'
|
||||
publish_image = 'grafana/grafana-ci-deploy:1.3.1'
|
||||
deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image'
|
||||
alpine_image = 'alpine:3.15'
|
||||
@@ -64,12 +64,20 @@ def initialize_step(edition, platform, ver_mode, is_downstream=False, install_de
|
||||
if ver_mode == 'release':
|
||||
committish = '${DRONE_TAG}'
|
||||
source_commit = ' ${DRONE_TAG}'
|
||||
environment = {
|
||||
'GITHUB_TOKEN': from_secret(github_token),
|
||||
}
|
||||
token = "--github-token $${GITHUB_TOKEN}"
|
||||
elif ver_mode == 'release-branch':
|
||||
committish = '${DRONE_BRANCH}'
|
||||
environment = {}
|
||||
token = ""
|
||||
else:
|
||||
environment = {}
|
||||
if is_downstream:
|
||||
source_commit = ' $${SOURCE_COMMIT}'
|
||||
committish = '${DRONE_COMMIT}'
|
||||
token = ""
|
||||
steps = [
|
||||
identify_runner,
|
||||
clone_enterprise(committish),
|
||||
@@ -79,14 +87,12 @@ def initialize_step(edition, platform, ver_mode, is_downstream=False, install_de
|
||||
'depends_on': [
|
||||
'clone-enterprise',
|
||||
],
|
||||
'environment': {
|
||||
'GITHUB_TOKEN': from_secret(github_token),
|
||||
},
|
||||
'environment': environment,
|
||||
'commands': [
|
||||
'mv bin/grabpl /tmp/',
|
||||
'rmdir bin',
|
||||
'mv grafana-enterprise /tmp/',
|
||||
'/tmp/grabpl init-enterprise --github-token $${{GITHUB_TOKEN}} /tmp/grafana-enterprise{}'.format(source_commit),
|
||||
'/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format(token, source_commit),
|
||||
'mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json',
|
||||
'mkdir bin',
|
||||
'mv /tmp/grabpl bin/'
|
||||
@@ -183,7 +189,7 @@ def enterprise_downstream_step(edition):
|
||||
'grafana/grafana-enterprise@main',
|
||||
],
|
||||
'params': [
|
||||
'SOURCE_BUILD_NUMBER=${DRONE_BUILD_NUMBER}',
|
||||
'SOURCE_BUILD_NUMBER=${DRONE_COMMIT}',
|
||||
'SOURCE_COMMIT=${DRONE_COMMIT}',
|
||||
],
|
||||
},
|
||||
@@ -421,9 +427,12 @@ def build_frontend_docs_step(edition):
|
||||
'name': 'build-frontend-docs',
|
||||
'image': build_image,
|
||||
'depends_on': [
|
||||
'build-frontend'
|
||||
'initialize'
|
||||
],
|
||||
'commands': [
|
||||
'yarn packages:build',
|
||||
'yarn packages:docsExtract',
|
||||
'yarn packages:docsToMarkdown',
|
||||
'./scripts/ci-reference-docs-lint.sh ci',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1006,6 +1006,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-properties@npm:7.16.7, @babel/plugin-proposal-class-properties@npm:^7.16.7":
|
||||
version: 7.16.7
|
||||
resolution: "@babel/plugin-proposal-class-properties@npm:7.16.7"
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin": ^7.16.7
|
||||
"@babel/helper-plugin-utils": ^7.16.7
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 3977e841e17b45b47be749b9a5b67b9e8b25ff0840f9fdad3f00cbcb35db4f5ff15f074939fe19b01207a29688c432cc2c682351959350834d62920b7881f803
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-properties@npm:^7.12.1, @babel/plugin-proposal-class-properties@npm:^7.13.0, @babel/plugin-proposal-class-properties@npm:^7.14.5":
|
||||
version: 7.14.5
|
||||
resolution: "@babel/plugin-proposal-class-properties@npm:7.14.5"
|
||||
@@ -1018,18 +1030,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-properties@npm:^7.16.7":
|
||||
version: 7.16.7
|
||||
resolution: "@babel/plugin-proposal-class-properties@npm:7.16.7"
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin": ^7.16.7
|
||||
"@babel/helper-plugin-utils": ^7.16.7
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 3977e841e17b45b47be749b9a5b67b9e8b25ff0840f9fdad3f00cbcb35db4f5ff15f074939fe19b01207a29688c432cc2c682351959350834d62920b7881f803
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-static-block@npm:^7.15.4":
|
||||
version: 7.15.4
|
||||
resolution: "@babel/plugin-proposal-class-static-block@npm:7.15.4"
|
||||
@@ -19572,6 +19572,7 @@ __metadata:
|
||||
resolution: "grafana@workspace:."
|
||||
dependencies:
|
||||
"@babel/core": 7.16.7
|
||||
"@babel/plugin-proposal-class-properties": 7.16.7
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": 7.16.7
|
||||
"@babel/plugin-proposal-object-rest-spread": 7.16.7
|
||||
"@babel/plugin-proposal-optional-chaining": 7.16.7
|
||||
|
||||
Reference in New Issue
Block a user