Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2024-11-19 05:02:01 +03:00
commit 78afd65998
52 files changed
+1299 -277

No files matched your search

+1 -1
View File
@@ -948,7 +948,7 @@ session_duration = "15m"
# Set the plugins that will receive AWS settings for each request (via plugin context)
# By default this will include all Grafana Labs owned AWS plugins, or those that make use of AWS settings (ElasticSearch, Prometheus).
forward_settings_to_plugins = cloudwatch, grafana-athena-datasource, grafana-redshift-datasource, grafana-x-ray-datasource, grafana-timestream-datasource, grafana-iot-sitewise-datasource, grafana-iot-twinmaker-app, grafana-opensearch-datasource, aws-datasource-provisioner, elasticsearch, prometheus
forward_settings_to_plugins = cloudwatch, grafana-athena-datasource, grafana-redshift-datasource, grafana-x-ray-datasource, grafana-timestream-datasource, grafana-iot-sitewise-datasource, grafana-iot-twinmaker-app, grafana-opensearch-datasource, aws-datasource-provisioner, elasticsearch, prometheus, grafana-amazonprometheus-datasource, grafana-aurora-datasource
#################################### Azure ###############################
[azure]
@@ -66,8 +66,17 @@ The metrics query editor helps you select metrics, group and aggregate by labels
1. _(Optional)_ Use the plus and minus icons in the filter and group-by sections to add and remove filters or group-by clauses.
Google Cloud Monitoring supports several metrics types, such as `GAUGE`, `DELTA,` and `CUMULATIVE`.
Each supports different aggregation options, such as reducers and aligners.
The metrics query editor lists available aggregation methods for a selected metric, and sets a default reducer and aligner when you select a metric.
Each supports different aggregation options, such as reducers and aligners. Additionally, metrics have specific value types that can be either scalar or a distribution.
The metrics query editor lists available aggregation methods for a selected metric, and sets a default aggregation, reducer and aligner when you select a metric.
In the case that the metric value type is a distribution, the aggregation will be set by default to the mean. For scalar value types, there is no aggregation by default.
The various metrics are documented [here](https://cloud.google.com/monitoring/api/metrics_gcp) and further details on the kinds and types of metrics can be found [here](https://cloud.google.com/monitoring/api/v3/kinds-and-types).
{{% admonition type="note" %}}
Distribution metrics are typically best visualized as either a heatmap or histogram. When visualizing in this way, aggregation is not necessary. However, for other visualization types, performance degradation may be observed when attempting to query distribution metrics that are not aggregated due to the number of potential buckets that can be returned. For more information on how to visualize distribution metrics refer to [this page](https://cloud.google.com/monitoring/charts/charting-distribution-metrics).
{{% /admonition %}}
### Apply a filter
@@ -32,7 +32,7 @@ _Logs_ are structured records of events or messages generated by a system or app
The logs visualization displays these records from data sources that support logs, such as Elastic, Influx, and Loki. The logs visualization has colored indicators of log status, as well as collapsible log events that help you analyze the information generated.
{{< figure src="/static/img/docs/v64/logs-panel.png" max-width="1025px" alt="Logs panel" >}}
![Logs visualization](/media/docs/grafana/panels-visualizations/screenshot-logs-v11.3.png)
{{< docs/play title="Logs Panel" url="https://play.grafana.org/d/6NmftOxZz/" >}}
@@ -46,7 +46,7 @@ The following video provides a walkthrough of creating a logs visualization. You
## Supported data formats
The logs visualization works best with log-type datasets such as queries from data sources like Loki, Elastic, and InlfuxDB.
The logs visualization works best with log-type datasets such as queries from data sources like Loki, Elastic, and InfluxDB.
You can also build log-formatted data from other data sources as long as the first field is a time type followed by string, number, and time fields. The leading time field is used to sort and timestamp the logs and if the data contains other time-type fields, they’re included as elements of the logged record.
@@ -66,27 +66,33 @@ To limit the number of log lines rendered in the visualization, you can use the
![Logs Example](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.1-logs-example.png 'Logs Example')
## Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
## Log level
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:log-levels).
## Log details
## Configuration options
Each log row has an extendable area with its labels and detected fields, for more robust interaction. Each field or label has a stats icon to display ad-hoc statistics in relation to all displayed logs.
{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="<GRAFANA_VERSION>" >}}
### Display options
### Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
### Logs options
Use these settings to refine your visualization:
- **Time -** Show or hide the time column. This is the timestamp associated with the log line as reported from the data source.
- **Unique labels -** Show or hide the unique labels column, which shows only non-common labels.
- **Common labels -** Show or hide the common labels.
- **Wrap lines -** Toggle line wrapping.
- **Prettify JSON -** Set this to `true` to pretty print all JSON logs. This setting does not affect logs in any format other than JSON.
- **Enable log details -** Toggle option to see the log details view for each log row. The default setting is true.
- **Deduplication -** Hides log messages that are duplicates of others shown according to your selected criteria. Choose from: **Exact** (ignoring ISO datetimes), **Numerical** (ignoring only those that differ by numbers such as IPs or latencies), or **Signatures** (removing successive lines with identical punctuation and white space).
- **Order -** Display results in descending or ascending time order. The default is **Descending**, showing the newest logs first. Set to **Ascending** to show the oldest log lines first.
<!-- prettier-ignore-start -->
| Option | Description |
| ------ | ----------- |
| Time | Show or hide the time column. This is the timestamp associated with the log line as reported from the data source. |
| Unique labels | Show or hide the unique labels column, which shows only non-common labels. |
| Common labels | Show or hide the common labels. |
| Wrap lines | Turn line wrapping on or off. |
| Prettify JSON | Toggle the switch on to pretty print all JSON logs. This setting does not affect logs in any format other than JSON. |
| Enable log details | Toggle the switch on to see an extendable area with log details including labels and detected fields. Each field or label has a stats icon to display ad-hoc statistics in relation to all displayed logs. The default setting is on. |
| Deduplication | Hide log messages that are duplicates of others shown, according to your selected criteria. Choose from: <ul><li>**Exact** - Ignoring ISO datetimes.</li><li>**Numerical** - Ignoring only those that differ by numbers such as IPs or latencies.</li><li>**Signatures** - Removing successive lines with identical punctuation and white space.</li></ul> |
| Order | Set whether to show results **Newest first** or **Oldest first**. |
<!-- prettier-ignore-end -->
@@ -3,8 +3,8 @@ import { config } from '@grafana/runtime';
import { SUGGESTIONS_LIMIT } from '../../../language_provider';
import { FUNCTIONS } from '../../../promql';
import { getCompletions } from './completions';
import { DataProvider, DataProviderParams } from './data_provider';
import { filterMetricNames, getCompletions } from './completions';
import { DataProvider, type DataProviderParams } from './data_provider';
import type { Situation } from './situation';
const history: string[] = ['previous_metric_name_1', 'previous_metric_name_2', 'previous_metric_name_3'];
@@ -41,6 +41,133 @@ afterEach(() => {
jest.restoreAllMocks();
});
describe('filterMetricNames', () => {
const sampleMetrics = [
'http_requests_total',
'http_requests_failed',
'node_cpu_seconds_total',
'node_memory_usage_bytes',
'very_long_metric_name_with_many_underscores_and_detailed_description',
'metric_name_1_with_extra_terms_included',
];
describe('empty input', () => {
it('should return all metrics up to limit when input is empty', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: '',
limit: 3,
});
expect(result).toEqual(sampleMetrics.slice(0, 3));
});
it('should return all metrics when input is whitespace', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: ' ',
limit: 3,
});
expect(result).toEqual(sampleMetrics.slice(0, 3));
});
});
describe('simple searches (≤ 4 terms)', () => {
it('should match exact strings', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_requests_total',
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
});
it('should match with single character errors', () => {
// substitution
let result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_requezts_total', // 's' replaced with 'z'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// ransposition
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqeust_total', // 'ue' swapped
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// deletion
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqests_total', // missing 'u'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// insertion
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqquests_total', // extra 'q'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
});
it('should match partial strings', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'requests', // partial match
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
expect(result).toContainEqual('http_requests_failed');
});
it('should not match with multiple errors', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'htp_reqests_total', // two errors: missing 't' and missing 'u'
limit: 10,
});
expect(result).not.toContainEqual('http_requests_total');
});
});
describe('complex searches (> 4 terms)', () => {
it('should use substring matching for each term', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'metric name 1 with extra terms',
limit: 10,
});
expect(result).toContainEqual('metric_name_1_with_extra_terms_included');
});
it('should return empty array when no metrics match all terms', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'metric name 1 with nonexistent terms',
limit: 10,
});
expect(result).toHaveLength(0);
});
it('should stop searching after limit is reached', () => {
const manyMetrics = Array.from({ length: 10 }, (_, i) => `metric_name_${i}_with_terms`);
const result = filterMetricNames({
metricNames: manyMetrics,
inputText: 'metric name with terms other words', // > 4 terms
limit: 3,
});
expect(result.length).toBeLessThanOrEqual(3);
});
});
});
type MetricNameSituation = Extract<Situation['type'], 'AT_ROOT' | 'EMPTY' | 'IN_FUNCTION'>;
const metricNameCompletionSituations = ['AT_ROOT', 'IN_FUNCTION', 'EMPTY'] as MetricNameSituation[];
@@ -74,22 +201,22 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
it('should limit completions for metric names when the number of metric names is greater than the limit', async () => {
it('should limit completions for metric names when the number exceeds the limit', async () => {
const situation: Situation = {
type: situationType,
};
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
// No text input
dataProvider.monacoSettings.setInputInRange('');
// Complex query
dataProvider.monacoSettings.setInputInRange('metric name one two three four five');
let completions = await getCompletions(situation, dataProvider);
expect(completions).toHaveLength(expectedCompletionsCount);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
// With text input (use fuzzy search)
dataProvider.monacoSettings.setInputInRange('name_1');
// Simple query with fuzzy match
dataProvider.monacoSettings.setInputInRange('metric_name_');
completions = await getCompletions(situation, dataProvider);
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
it('should enable autocomplete suggestions update when the number of metric names is greater than the limit', async () => {
@@ -115,4 +242,35 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat
await getCompletions(situation, dataProvider);
expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true);
});
it('should handle complex queries efficiently', async () => {
const situation: Situation = {
type: situationType,
};
const testMetrics = ['metric_name_1', 'metric_name_2', 'metric_name_1_with_extra_terms', 'unrelated_metric'];
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(testMetrics);
// Test with a complex query (> 4 terms)
dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more');
const completions = await getCompletions(situation, dataProvider);
const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME');
expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true);
});
it('should handle multiple term queries efficiently', async () => {
const situation: Situation = {
type: situationType,
};
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
// Test with multiple terms
dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5');
const completions = await getCompletions(situation, dataProvider);
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
});
@@ -22,7 +22,31 @@ type Completion = {
triggerOnInsert?: boolean;
};
const metricNamesSearchClient = new UFuzzy({ intraMode: 1 });
const metricNamesSearch = {
// see https://github.com/leeoniya/uFuzzy?tab=readme-ov-file#how-it-works for details
multiInsert: new UFuzzy({ intraMode: 0 }),
singleError: new UFuzzy({ intraMode: 1 }),
};
interface MetricFilterOptions {
metricNames: string[];
inputText: string;
limit: number;
}
export function filterMetricNames({ metricNames, inputText, limit }: MetricFilterOptions): string[] {
if (!inputText?.trim()) {
return metricNames.slice(0, limit);
}
const terms = metricNamesSearch.multiInsert.split(inputText); // e.g. 'some_metric_name or-another' -> ['some', 'metric', 'name', 'or', 'another']
const isComplexSearch = terms.length > 4;
const fuzzyResults = isComplexSearch
? metricNamesSearch.multiInsert.filter(metricNames, inputText) // for complex searches, prioritize performance by using MultiInsert fuzzy search
: metricNamesSearch.singleError.filter(metricNames, inputText); // for simple searches, prioritize flexibility by using SingleError fuzzy search
return fuzzyResults ? fuzzyResults.slice(0, limit).map((idx) => metricNames[idx]) : [];
}
// we order items like: history, functions, metrics
function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[] {
@@ -36,11 +60,11 @@ function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[]
monacoSettings.enableAutocompleteSuggestionsUpdate();
if (monacoSettings.inputInRange) {
metricNames =
metricNamesSearchClient
.filter(metricNames, monacoSettings.inputInRange)
?.slice(0, dataProvider.metricNamesSuggestionLimit)
.map((idx) => metricNames[idx]) ?? [];
metricNames = filterMetricNames({
metricNames,
inputText: monacoSettings.inputInRange,
limit: dataProvider.metricNamesSuggestionLimit,
});
} else {
metricNames = metricNames.slice(0, dataProvider.metricNamesSuggestionLimit);
}
+2 -1
View File
@@ -6,7 +6,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"allowJs": true,
"rootDirs": ["."]
"rootDirs": ["."],
"resolveJsonModule": true
},
"exclude": ["dist/**/*"],
"extends": "@grafana/tsconfig",
@@ -1,12 +1,15 @@
import { createContext, useContext } from 'react';
import { useObservable } from 'react-use';
import { locationService as mainLocationService } from './LocationService';
import { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL';
export const SidecarContext_EXPERIMENTAL = createContext<SidecarService_EXPERIMENTAL>(
sidecarServiceSingleton_EXPERIMENTAL
);
const HIDDEN_ROUTES = ['/login'];
/**
* This is the main way to interact with the sidecar service inside a react context. It provides a wrapper around the
* service props so that even though they are observables we just pass actual values to the components.
@@ -24,17 +27,33 @@ export function useSidecar_EXPERIMENTAL() {
const initialContext = useObservable(service.initialContextObservable, service.initialContext);
const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId);
const locationService = service.getLocationService();
const forceHidden = HIDDEN_ROUTES.includes(mainLocationService.getLocation().pathname);
return {
activePluginId,
initialContext,
activePluginId: forceHidden ? undefined : activePluginId,
initialContext: forceHidden ? undefined : initialContext,
locationService,
// TODO: currently this allows anybody to open any app, in the future we should probably scope this to the
// current app but that means we will need to incorporate this better into the plugin platform APIs which
// we will do once the functionality is reasonably stable
openApp: (pluginId: string, context?: unknown) => service.openApp(pluginId, context),
openAppV2: (pluginId: string, path?: string) => service.openAppV2(pluginId, path),
openApp: (pluginId: string, context?: unknown) => {
if (forceHidden) {
return;
}
return service.openApp(pluginId, context);
},
openAppV2: (pluginId: string, path?: string) => {
if (forceHidden) {
return;
}
return service.openAppV2(pluginId, path);
},
closeApp: () => service.closeApp(),
isAppOpened: (pluginId: string) => service.isAppOpened(pluginId),
isAppOpened: (pluginId: string) => {
if (forceHidden) {
return false;
}
return service.isAppOpened(pluginId);
},
};
}
@@ -2,6 +2,7 @@ import * as H from 'history';
import { pick } from 'lodash';
import { BehaviorSubject, map, Observable } from 'rxjs';
import { reportInteraction } from '../analytics/utils';
import { config } from '../config';
import { HistoryWrapper, LocationService, locationService } from './LocationService';
@@ -92,6 +93,8 @@ export class SidecarService_EXPERIMENTAL {
}
this._initialContext.next(context);
this.memoryLocationService.push({ pathname: `/a/${pluginId}` });
reportInteraction('sidecar_service_open_app', { pluginId, version: 1 });
}
/**
@@ -104,6 +107,7 @@ export class SidecarService_EXPERIMENTAL {
}
this.memoryLocationService.push({ pathname: `/a/${pluginId}${path || ''}` });
reportInteraction('sidecar_service_open_app', { pluginId, version: 2 });
}
/**
@@ -116,6 +120,8 @@ export class SidecarService_EXPERIMENTAL {
this._initialContext.next(undefined);
this.memoryLocationService.replace({ pathname: '/' });
reportInteraction('sidecar_service_close_app');
}
/**
@@ -139,7 +145,9 @@ export class SidecarService_EXPERIMENTAL {
return false;
}
return !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId));
const result = !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId));
reportInteraction('sidecar_service_is_app_opened', { pluginId, isOpened: result });
return result;
}
}
@@ -31,19 +31,16 @@ type UPlotChartState = {
export class UPlotChart extends Component<PlotProps, UPlotChartState> {
plotContainer = createRef<HTMLDivElement>();
plotCanvasBBox = createRef<DOMRect>();
plotInstance: uPlot | null = null;
constructor(props: PlotProps) {
super(props);
this.state = {
plot: null,
};
}
reinitPlot() {
let { width, height, plotRef } = this.props;
this.state.plot?.destroy();
this.plotInstance?.destroy();
if (width === 0 && height === 0) {
return;
@@ -69,7 +66,7 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
plotRef(plot);
}
this.setState({ plot });
this.plotInstance = plot;
}
componentDidMount() {
@@ -77,21 +74,19 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
}
componentWillUnmount() {
this.state.plot?.destroy();
this.plotInstance?.destroy();
}
componentDidUpdate(prevProps: PlotProps) {
let { plot } = this.state;
if (!sameDims(prevProps, this.props)) {
plot?.setSize({
this.plotInstance?.setSize({
width: Math.floor(this.props.width),
height: Math.floor(this.props.height),
});
} else if (!sameConfig(prevProps, this.props)) {
this.reinitPlot();
} else if (!sameData(prevProps, this.props)) {
plot?.setData(this.props.data as AlignedData);
this.plotInstance?.setData(this.props.data as AlignedData);
}
}
+2 -2
View File
@@ -838,14 +838,14 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
ac, folderSvc, fStore, nil,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(),
)
require.NoError(t, err)
}
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
ac, folderSvc, fStore, nil,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(),
)
require.NoError(t, err)
+1 -1
View File
@@ -478,7 +478,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
sc.cfg, dashStore, folderStore,
features, folderPermissions, dashboardPermissions, ac,
folderServiceWithFlagOn, fStore, nil,
folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(),
)
require.NoError(b, err)
-4
View File
@@ -9,10 +9,6 @@ import (
type DB struct {
}
func (db *DB) TablesList(rawSQL string) ([]string, error) {
return nil, errors.New("not implemented")
}
func (db *DB) RunCommands(commands []string) (string, error) {
return "", errors.New("not implemented")
}
+2 -2
View File
@@ -20,10 +20,10 @@ var logger = log.New("sql_expr")
// TablesList returns a list of tables for the sql statement
func TablesList(rawSQL string) ([]string, error) {
duckDB := NewInMemoryDB()
db := NewInMemoryDB()
rawSQL = strings.Replace(rawSQL, "'", "''", -1)
cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL)
ret, err := duckDB.RunCommands([]string{cmd})
ret, err := db.RunCommands([]string{cmd})
if err != nil {
logger.Error("error serializing sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd)
return nil, fmt.Errorf("error serializing sql: %s", err.Error())
+333 -38
View File
@@ -769,6 +769,242 @@ func (*WriteResponse) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{12}
}
type BatchCheckRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
}
func (x *BatchCheckRequest) Reset() {
*x = BatchCheckRequest{}
mi := &file_extention_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchCheckRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckRequest) ProtoMessage() {}
func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead.
func (*BatchCheckRequest) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{13}
}
func (x *BatchCheckRequest) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *BatchCheckRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *BatchCheckRequest) GetItems() []*BatchCheckItem {
if x != nil {
return x.Items
}
return nil
}
type BatchCheckItem struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
}
func (x *BatchCheckItem) Reset() {
*x = BatchCheckItem{}
mi := &file_extention_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchCheckItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckItem) ProtoMessage() {}
func (x *BatchCheckItem) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead.
func (*BatchCheckItem) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{14}
}
func (x *BatchCheckItem) GetVerb() string {
if x != nil {
return x.Verb
}
return ""
}
func (x *BatchCheckItem) GetGroup() string {
if x != nil {
return x.Group
}
return ""
}
func (x *BatchCheckItem) GetResource() string {
if x != nil {
return x.Resource
}
return ""
}
func (x *BatchCheckItem) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *BatchCheckItem) GetSubresource() string {
if x != nil {
return x.Subresource
}
return ""
}
func (x *BatchCheckItem) GetFolder() string {
if x != nil {
return x.Folder
}
return ""
}
type BatchCheckResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *BatchCheckResponse) Reset() {
*x = BatchCheckResponse{}
mi := &file_extention_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchCheckResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckResponse) ProtoMessage() {}
func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead.
func (*BatchCheckResponse) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{15}
}
func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource {
if x != nil {
return x.Groups
}
return nil
}
type BatchCheckGroupResource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
func (x *BatchCheckGroupResource) Reset() {
*x = BatchCheckGroupResource{}
mi := &file_extention_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BatchCheckGroupResource) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckGroupResource) ProtoMessage() {}
func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead.
func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{16}
}
func (x *BatchCheckGroupResource) GetItems() map[string]bool {
if x != nil {
return x.Items
}
return nil
}
var File_extention_proto protoreflect.FileDescriptor
var file_extention_proto_rawDesc = []byte{
@@ -874,27 +1110,74 @@ var file_extention_proto_rawDesc = []byte{
0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73,
0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x32, 0xfb, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c,
0x69, 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65,
0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74,
0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f,
0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63,
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12,
0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22,
0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e,
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f,
0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74,
0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74,
0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x0e, 0x42, 0x61,
0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04,
0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62,
0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64,
0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72,
0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74,
0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74,
0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65,
0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68,
0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x17,
0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65,
0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63,
0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05,
0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
0xd8, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69,
0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c, 0x69, 0x73,
0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74,
0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68,
0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42,
0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68,
0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74,
0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e,
0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61,
0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76,
0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72,
0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b,
0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05,
0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78,
0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61,
0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -909,7 +1192,7 @@ func file_extention_proto_rawDescGZIP() []byte {
return file_extention_proto_rawDescData
}
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_extention_proto_goTypes = []any{
(*ListRequest)(nil), // 0: authz.extention.v1.ListRequest
(*ListResponse)(nil), // 1: authz.extention.v1.ListResponse
@@ -924,33 +1207,45 @@ var file_extention_proto_goTypes = []any{
(*WriteRequestDeletes)(nil), // 10: authz.extention.v1.WriteRequestDeletes
(*WriteRequest)(nil), // 11: authz.extention.v1.WriteRequest
(*WriteResponse)(nil), // 12: authz.extention.v1.WriteResponse
(*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp
(*structpb.Struct)(nil), // 14: google.protobuf.Struct
(*wrapperspb.Int32Value)(nil), // 15: google.protobuf.Int32Value
(*BatchCheckRequest)(nil), // 13: authz.extention.v1.BatchCheckRequest
(*BatchCheckItem)(nil), // 14: authz.extention.v1.BatchCheckItem
(*BatchCheckResponse)(nil), // 15: authz.extention.v1.BatchCheckResponse
(*BatchCheckGroupResource)(nil), // 16: authz.extention.v1.BatchCheckGroupResource
nil, // 17: authz.extention.v1.BatchCheckResponse.GroupsEntry
nil, // 18: authz.extention.v1.BatchCheckGroupResource.ItemsEntry
(*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp
(*structpb.Struct)(nil), // 20: google.protobuf.Struct
(*wrapperspb.Int32Value)(nil), // 21: google.protobuf.Int32Value
}
var file_extention_proto_depIdxs = []int32{
5, // 0: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition
2, // 1: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey
13, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp
14, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct
19, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp
20, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct
7, // 4: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey
15, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value
21, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value
3, // 6: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple
2, // 7: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey
4, // 8: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition
9, // 9: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites
10, // 10: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes
0, // 11: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
6, // 12: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest
11, // 13: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest
1, // 14: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
8, // 15: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse
12, // 16: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse
14, // [14:17] is the sub-list for method output_type
11, // [11:14] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
14, // 11: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem
17, // 12: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry
18, // 13: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry
16, // 14: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource
0, // 15: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
13, // 16: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest
6, // 17: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest
11, // 18: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest
1, // 19: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
15, // 20: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse
8, // 21: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse
12, // 22: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse
19, // [19:23] is the sub-list for method output_type
15, // [15:19] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
}
func init() { file_extention_proto_init() }
@@ -964,7 +1259,7 @@ func file_extention_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_extention_proto_rawDesc,
NumEnums: 0,
NumMessages: 13,
NumMessages: 19,
NumExtensions: 0,
NumServices: 1,
},
@@ -10,6 +10,8 @@ import "google/protobuf/wrappers.proto";
service AuthzExtentionService {
rpc List(ListRequest) returns (ListResponse);
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
rpc Read(ReadRequest) returns (ReadResponse);
rpc Write(WriteRequest) returns (WriteResponse);
}
@@ -86,3 +88,26 @@ message WriteRequest {
}
message WriteResponse {}
message BatchCheckRequest {
string subject = 1;
string namespace = 2;
repeated BatchCheckItem items = 3;
}
message BatchCheckItem {
string verb = 1;
string group = 2;
string resource = 3;
string name = 4;
string subresource = 5;
string folder = 6;
}
message BatchCheckResponse {
map<string, BatchCheckGroupResource> groups = 1;
}
message BatchCheckGroupResource {
map<string, bool> items = 1;
}
@@ -19,9 +19,10 @@ import (
const _ = grpc.SupportPackageIsVersion8
const (
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
)
// AuthzExtentionServiceClient is the client API for AuthzExtentionService service.
@@ -29,6 +30,7 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AuthzExtentionServiceClient interface {
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
}
@@ -51,6 +53,16 @@ func (c *authzExtentionServiceClient) List(ctx context.Context, in *ListRequest,
return out, nil
}
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchCheckResponse)
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReadResponse)
@@ -76,6 +88,7 @@ func (c *authzExtentionServiceClient) Write(ctx context.Context, in *WriteReques
// for forward compatibility
type AuthzExtentionServiceServer interface {
List(context.Context, *ListRequest) (*ListResponse, error)
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
Read(context.Context, *ReadRequest) (*ReadResponse, error)
Write(context.Context, *WriteRequest) (*WriteResponse, error)
}
@@ -87,6 +100,9 @@ type UnimplementedAuthzExtentionServiceServer struct {
func (UnimplementedAuthzExtentionServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
}
func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
}
@@ -123,6 +139,24 @@ func _AuthzExtentionService_List_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchCheckRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReadRequest)
if err := dec(in); err != nil {
@@ -170,6 +204,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
MethodName: "List",
Handler: _AuthzExtentionService_List_Handler,
},
{
MethodName: "BatchCheck",
Handler: _AuthzExtentionService_BatchCheck_Handler,
},
{
MethodName: "Read",
Handler: _AuthzExtentionService_Read_Handler,
+1
View File
@@ -16,6 +16,7 @@ type Client interface {
List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error)
Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error)
Write(ctx context.Context, req *authzextv1.WriteRequest) error
BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
}
func NewNoopClient() *client.NoopClient {
@@ -169,3 +169,10 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error
_, err := c.authzext.Write(ctx, req)
return err
}
func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check")
defer span.End()
return c.authzext.BatchCheck(ctx, req)
}
@@ -36,3 +36,7 @@ func (nc NoopClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*au
func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) error {
return nil
}
func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
return nil, nil
}
@@ -0,0 +1,64 @@
package server
import (
"context"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.BatchCheck")
defer span.End()
batchRes := &authzextv1.BatchCheckResponse{
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
}
subject := r.GetSubject()
for _, item := range r.Items {
groupPrefix := common.FormatGroupResource(item.GetGroup(), item.GetResource())
allowed, err := s.batchCheckItem(ctx, subject, r.Namespace, item)
if err != nil {
return nil, err
}
if _, ok := batchRes.Groups[groupPrefix]; !ok {
batchRes.Groups[groupPrefix] = &authzextv1.BatchCheckGroupResource{
Items: make(map[string]bool),
}
}
batchRes.Groups[groupPrefix].Items[item.GetName()] = allowed
}
return batchRes, nil
}
func (s *Server) batchCheckItem(ctx context.Context, subject string, namespace string, item *authzextv1.BatchCheckItem) (bool, error) {
req := &authzv1.CheckRequest{
Namespace: namespace,
Subject: subject,
Verb: item.GetVerb(),
Group: item.GetGroup(),
Resource: item.GetResource(),
Name: item.GetName(),
Folder: item.GetFolder(),
Subresource: item.GetSubresource(),
}
var res *authzv1.CheckResponse
var err error
if info, ok := common.GetTypeInfo(item.GetGroup(), item.GetResource()); ok {
res, err = s.checkTyped(ctx, req, info)
} else {
res, err = s.checkGeneric(ctx, req)
}
if err != nil {
return false, err
}
return res.Allowed, nil
}
@@ -0,0 +1,120 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/utils"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
)
func newBatch(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
for i, item := range items {
items[i] = &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Group: group,
Resource: resource,
Name: item.GetName(),
Folder: item.GetFolder(),
}
}
return &authzextv1.BatchCheckRequest{
Namespace: "default",
Subject: subject,
Items: items,
}
}
func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:1 should only be able to read resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
})
t.Run("user:2 should be able to read resource:dashboards.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
assert.Len(t, res.Groups[groupPrefix].Items, 2)
})
t.Run("user:3 should be able to read resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
})
t.Run("user:4 should be able to read all dashboards.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "3"},
{Name: "3", Folder: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 3)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.True(t, res.Groups[groupPrefix].Items["2"])
assert.False(t, res.Groups[groupPrefix].Items["3"])
})
t.Run("user:5 should be able to read resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
})
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
})
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newBatch("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
})
}
@@ -20,6 +20,31 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
return s.checkGeneric(ctx, r)
}
// checkNamespace checks if subject has access through namespace
func (s *Server) checkNamespace(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err
}
relation := common.VerbMapping[r.GetVerb()]
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: storeInf.Id,
AuthorizationModelId: storeInf.AuthorizationModelId,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
return nil, err
}
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) {
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
@@ -47,20 +72,12 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info c
}
// 2. check if subject has access through namespace
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: storeInf.Id,
AuthorizationModelId: storeInf.AuthorizationModelId,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
nsRes, err := s.checkNamespace(ctx, r)
if err != nil {
return nil, err
}
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
return &authzv1.CheckResponse{Allowed: nsRes.GetAllowed()}, nil
}
func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
@@ -96,21 +113,12 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au
}
// 2. check if subject has access through namespace
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: storeInf.Id,
AuthorizationModelId: storeInf.AuthorizationModelId,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
nsRes, err := s.checkNamespace(ctx, r)
if err != nil {
return nil, err
}
if res.GetAllowed() {
if nsRes.GetAllowed() {
return &authzv1.CheckResponse{Allowed: true}, nil
}
@@ -49,6 +49,10 @@ func TestIntegrationServer(t *testing.T) {
t.Run("test list", func(t *testing.T) {
testList(t, srv)
})
t.Run("test batch check", func(t *testing.T) {
testBatchCheck(t, srv)
})
}
func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
+10
View File
@@ -83,6 +83,8 @@ var (
ToOpenFGATuples = common.ToOpenFGATuples
ToOpenFGATupleKey = common.ToOpenFGATupleKey
ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition
FormatGroupResource = common.FormatGroupResource
)
// NewTupleEntry constructs new openfga entry type:name[#relation].
@@ -166,6 +168,14 @@ func TranslateToCheckRequest(namespace, action, kind, folder, name string) (*aut
return req, true
}
func TranslateToGroupResource(kind string) string {
translation, ok := resourceTranslations[kind]
if !ok {
return ""
}
return common.FormatGroupResource(translation.group, translation.resource)
}
func TranslateBasicRole(name string) string {
return basicRolesTranslations[name]
}
@@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/datasources"
@@ -59,6 +60,7 @@ type DashboardServiceImpl struct {
folderPermissions accesscontrol.FolderPermissionsService
dashboardPermissions accesscontrol.DashboardPermissionsService
ac accesscontrol.AccessControl
zclient zanzana.Client
metrics *dashboardsMetrics
}
@@ -67,7 +69,7 @@ func ProvideDashboardServiceImpl(
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client,
) (*DashboardServiceImpl, error) {
dashSvc := &DashboardServiceImpl{
cfg: cfg,
@@ -77,6 +79,7 @@ func ProvideDashboardServiceImpl(
folderPermissions: folderPermissionsService,
dashboardPermissions: dashboardPermissionsService,
ac: ac,
zclient: zclient,
folderStore: folderStore,
folderService: folderSvc,
metrics: newDashboardsMetrics(r),
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -883,6 +884,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
@@ -949,6 +951,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)
@@ -974,6 +977,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
_, err = service.SaveDashboard(context.Background(), &dto, false)
@@ -1018,6 +1022,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)
@@ -1069,6 +1074,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)
+115 -3
View File
@@ -6,9 +6,15 @@ import (
"github.com/prometheus/client_golang/prometheus"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
)
const (
defaultQueryLimit = 1000
)
type searchResult struct {
runner string
result []dashboards.DashboardSearchProjection
@@ -79,7 +85,113 @@ func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context
return first.result, first.err
}
func (dr *DashboardServiceImpl) findDashboardsZanzana(_ context.Context, _ dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
// FIXME: Implement using the new schema
return []dashboards.DashboardSearchProjection{}, nil
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
return dr.findDashboardsZanzanaCheck(ctx, query)
}
// findDashboardsZanzanaCheck implements "Search, then check" strategy. It first performs search query, then filters out results
// by checking access to each item.
func (dr *DashboardServiceImpl) findDashboardsZanzanaCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaCheck")
defer span.End()
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
query.SkipAccessControlFilter = true
// Remember initial query limit
limit := query.Limit
// Set limit to default to prevent pagination issues
query.Limit = defaultQueryLimit
if query.Page == 0 {
query.Page = 1
}
for len(result) < int(limit) {
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
remains := limit - int64(len(result))
res, err := dr.checkDashboardsBatch(ctx, query, findRes, remains)
if err != nil {
return nil, err
}
result = append(result, res...)
query.Page++
// Stop when last page reached
if len(findRes) < defaultQueryLimit {
break
}
}
return result, nil
}
func (dr *DashboardServiceImpl) checkDashboardsBatch(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.checkDashboardsBatch")
defer span.End()
if len(searchRes) == 0 {
return nil, nil
}
batchReqItems := make([]*authzextv1.BatchCheckItem, 0, len(searchRes))
for _, d := range searchRes {
// FIXME: support different access levels
kind := zanzana.KindDashboards
action := dashboards.ActionDashboardsRead
if d.IsFolder {
kind = zanzana.KindFolders
action = dashboards.ActionFoldersRead
}
checkReq, ok := zanzana.TranslateToCheckRequest("", action, kind, d.FolderUID, d.UID)
if !ok {
continue
}
batchReqItems = append(batchReqItems, &authzextv1.BatchCheckItem{
Verb: checkReq.Verb,
Group: checkReq.Group,
Resource: checkReq.Resource,
Name: checkReq.Name,
Folder: checkReq.Folder,
Subresource: checkReq.Subresource,
})
}
batchReq := authzextv1.BatchCheckRequest{
Namespace: query.SignedInUser.GetNamespace(),
Subject: query.SignedInUser.GetUID(),
Items: batchReqItems,
}
res, err := dr.zclient.BatchCheck(ctx, &batchReq)
if err != nil {
return nil, err
}
result := make([]dashboards.DashboardSearchProjection, 0)
for _, d := range searchRes {
if len(result) >= int(remains) {
break
}
kind := zanzana.KindDashboards
if d.IsFolder {
kind = zanzana.KindFolders
}
groupResource := zanzana.TranslateToGroupResource(kind)
if group, ok := res.Groups[groupResource]; ok {
if allowed := group.Items[d.UID]; allowed {
result = append(result, d)
}
}
}
return result, nil
}
@@ -12,6 +12,7 @@ import (
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
@@ -99,7 +100,7 @@ func TestValidateDashboardExists(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil))
require.NoError(t, err)
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil)
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient())
require.NoError(t, err)
s := ProvideService(dsStore, secretsService, dashSvc)
ctx := context.Background()
@@ -490,7 +490,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
CanEditValue: true,
})
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil)
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient())
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b)
@@ -573,7 +573,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
})
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil)
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient())
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
@@ -719,7 +719,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
tc.service.dashboardStore = dashStore
tc.service.store = nestedFolderStore
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil)
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient())
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b)
require.NoError(t, err)
@@ -1504,6 +1504,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
serviceWithFlagOn,
nestedFolderStore,
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
@@ -310,6 +310,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
)
require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
@@ -396,7 +397,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
cfg, dashboardStore, folderStore,
features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil,
nil, zanzana.NewNoopClient(),
)
require.NoError(t, svcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
@@ -458,7 +459,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashboardStore, folderStore,
features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil,
nil, zanzana.NewNoopClient(),
)
require.NoError(t, dashSvcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashService)
@@ -7,6 +7,9 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/bus"
@@ -19,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@@ -38,8 +42,6 @@ import (
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
const userInDbName = "user_in_db"
@@ -734,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
cfg, dashboardStore, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil,
nil, zanzana.NewNoopClient(),
)
require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
@@ -831,7 +833,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashStore, folderStore,
features, acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil,
nil, zanzana.NewNoopClient(),
)
require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashService)
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@@ -61,7 +62,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
cfg, dashboardStore, fs,
features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil,
nil, zanzana.NewNoopClient(),
)
require.NoError(tb, err)
@@ -10,12 +10,13 @@ import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -24,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/dashboards/service"
@@ -324,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
dashService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStoreService, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), nil,
foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(),
)
require.NoError(t, err)
@@ -184,13 +184,13 @@ func addAnnotationMig(mg *Migrator) {
Postgres("ALTER TABLE annotation ALTER COLUMN tags TYPE VARCHAR(4096);").
Mysql("ALTER TABLE annotation MODIFY tags VARCHAR(4096);"))
mg.AddMigration("Increase prev_state column to length 40", NewRawSQLMigration("").
Postgres("ALTER TABLE annotation ALTER COLUMN prev_state TYPE VARCHAR(40);").
Mysql("ALTER TABLE annotation MODIFY prev_state VARCHAR(40);"))
mg.AddMigration("Increase prev_state column to length 40 not null", NewRawSQLMigration("").
Postgres("ALTER TABLE annotation ALTER COLUMN prev_state TYPE VARCHAR(40);"). // Does not modify nullability.
Mysql("ALTER TABLE annotation MODIFY prev_state VARCHAR(40) NOT NULL;"))
mg.AddMigration("Increase new_state column to length 40", NewRawSQLMigration("").
Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);").
Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40);"))
mg.AddMigration("Increase new_state column to length 40 not null", NewRawSQLMigration("").
Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);"). // Does not modify nullability.
Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40) NOT NULL;"))
}
type AddMakeRegionSingleRowMigration struct {
+8 -1
View File
@@ -77,10 +77,17 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) {
e.logger.Info("Prepared request", "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest)
res, err := e.client.ExecuteMultisearch(req)
if err != nil {
// We are returning error containing the source that was added through errorsource.Middleware
if backend.IsDownstreamHTTPError(err) {
err = errorsource.DownstreamError(err, false)
}
return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, err), nil
}
if res.Status >= 400 {
errWithSource := errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.Status), fmt.Errorf("unexpected status code: %d", res.Status), false)
return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, errWithSource), nil
}
return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.keepLabelsInResponse, e.logger)
}
+1 -5
View File
@@ -20,7 +20,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
exp "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
exphttpclient "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource/httpclient"
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
)
@@ -90,10 +89,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
httpCliOpts.SigV4.Service = "es"
}
// set the default middlewars from the httpClientProvider
httpCliOpts.Middlewares = httpClientProvider.Opts.Middlewares
// enable experimental http client to support errors with source
httpCli, err := exphttpclient.New(httpCliOpts)
httpCli, err := httpClientProvider.New(httpCliOpts)
if err != nil {
return nil, err
}
@@ -4,6 +4,7 @@ import (
"errors"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
"github.com/stretchr/testify/require"
)
@@ -43,7 +44,8 @@ func TestErrorAvgMissingField(t *testing.T) {
require.NoError(t, err)
// FIXME: we should return the received error message
require.Len(t, result.response.Responses, 0)
require.Equal(t, "unexpected status code: 400", result.response.Responses["A"].Error.Error())
require.Equal(t, backend.ErrorSourceDownstream, result.response.Responses["A"].ErrorSource)
}
func TestErrorAvgMissingFieldNoDetailedErrors(t *testing.T) {
@@ -71,7 +73,7 @@ func TestErrorAvgMissingFieldNoDetailedErrors(t *testing.T) {
require.NoError(t, err)
// FIXME: we should return the received error message
require.Len(t, result.response.Responses, 0)
require.Equal(t, "unexpected status code: 400", result.response.Responses["A"].Error.Error())
}
func TestErrorTooManyDateHistogramBuckets(t *testing.T) {
@@ -44,7 +44,15 @@ export interface LoadDashboardOptions {
uid: string;
route: DashboardRoutes;
urlFolderUid?: string;
queryParams?: UrlQueryMap;
params?: {
version: number;
scopes: string[];
timeRange: {
from: string;
to: string;
};
variables: UrlQueryMap;
};
}
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
@@ -59,11 +67,11 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
uid,
route,
urlFolderUid,
queryParams,
params,
}: LoadDashboardOptions): Promise<DashboardDTO | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
if (!queryParams) {
if (!params) {
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
@@ -97,6 +105,15 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
}
default:
const queryParams = params
? {
version: params.version,
scopes: params.scopes,
from: params.timeRange.from,
to: params.timeRange.to,
...params.variables,
}
: undefined;
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams);
if (route === DashboardRoutes.Embedded) {
@@ -188,17 +205,26 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
}
public async reloadDashboard(queryParams?: LoadDashboardOptions['queryParams'] | undefined) {
if (!this.state.options) {
public async reloadDashboard(params: LoadDashboardOptions['params']) {
const stateOptions = this.state.options;
if (!stateOptions) {
return;
}
const options = {
...this.state.options,
queryParams,
...stateOptions,
params,
};
if (isEqual(options, this.state.options)) {
// We shouldn't check all params since:
// - version doesn't impact the new dashboard, and it's there for increased compatibility
// - time range is almost always different for relative time ranges and absolute time ranges do not trigger subsequent reloads
// - other params don't affect the dashboard content
if (
isEqual(options.params?.variables, stateOptions.params?.variables) &&
isEqual(options.params?.scopes, stateOptions.params?.scopes)
) {
return;
}
@@ -1,8 +1,8 @@
import { isEqual } from 'lodash';
import { debounce, isEqual } from 'lodash';
import { UrlQueryMap } from '@grafana/data';
import { sceneGraph, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes';
import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes';
import { getClosestScopesFacade } from 'app/features/scopes';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
@@ -13,27 +13,25 @@ export interface DashboardReloadBehaviorState extends SceneObjectState {
}
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
private _scopesFacade: ScopesFacade | null = null;
constructor(state: DashboardReloadBehaviorState) {
const shouldReload = state.reloadOnParamsChange && state.uid;
super(state);
this.reloadDashboard = this.reloadDashboard.bind(this);
// Sometimes the reload is triggered multiple subsequent times
// Debouncing it prevents double/triple reloads
this.reloadDashboard = debounce(this.reloadDashboard).bind(this);
if (shouldReload) {
this.addActivationHandler(() => {
this._scopesFacade = getClosestScopesFacade(this);
getClosestScopesFacade(this)?.setState({
handler: this.reloadDashboard,
});
this._variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: this.reloadDashboard,
});
this._scopesFacade?.setState({
handler: this.reloadDashboard,
});
this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => {
if (!isEqual(newState.value, prevState.value)) {
@@ -41,6 +39,8 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
}
})
);
this.reloadDashboard();
});
}
}
@@ -59,21 +59,25 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
if (!this.isEditing() && !this.isWaitingForVariables()) {
const timeRange = sceneGraph.getTimeRange(this);
let params: UrlQueryMap = {
version: this.state.version,
scopes: this._scopesFacade?.value.map((scope) => scope.metadata.name),
...timeRange.urlSync?.getUrlState(),
};
params = sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
(acc, variable) => ({
...acc,
...variable.urlSync?.getUrlState(),
}),
params
);
getDashboardScenePageStateManager().reloadDashboard(params);
// This is wrapped in setTimeout in order to allow variables and scopes to be set in the URL before actually reloading the dashboard
setTimeout(() => {
getDashboardScenePageStateManager().reloadDashboard({
version: this.state.version!,
scopes: getClosestScopesFacade(this)?.value.map((scope) => scope.metadata.name) ?? [],
// We're not using the getUrlState from timeRange since it makes more sense to pass the absolute timestamps as opposed to relative time
timeRange: {
from: timeRange.state.value.from.toISOString(),
to: timeRange.state.value.to.toISOString(),
},
variables: sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
(acc, variable) => ({
...acc,
...variable.urlSync?.getUrlState(),
}),
{}
),
});
});
}
}
}
@@ -1,7 +1,8 @@
import { config } from '@grafana/runtime';
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
import { clearMocks, enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@@ -15,84 +16,66 @@ jest.mock('@grafana/runtime', () => ({
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
}));
const runTest = async (
reloadDashboardsOnParamsChange: boolean,
reloadOnParamsChange: boolean,
withUid: boolean,
editMode: boolean
) => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
const uid = 'dash-1';
const dashboardScene = renderDashboard({ uid: withUid ? uid : undefined }, { reloadOnParamsChange });
if (editMode) {
await enterEditMode(dashboardScene);
}
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
await updateTimeRange(dashboardScene);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateMyVar(dashboardScene, '2');
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateScopes(['grafana']);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
};
describe('Dashboard reload', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
afterEach(async () => {
setDashboardAPI(undefined);
await resetScenes();
});
it.each([
[false, false, false, false],
[false, false, true, false],
[false, true, false, false],
[false, true, true, false],
[true, false, false, false],
[true, false, true, false],
[true, true, false, true],
[true, true, true, true],
[true, true, false, false],
[true, true, true, false],
])(
`reloadDashboardsOnParamsChange: %s, reloadOnParamsChange: %s, withUid: %s, editMode: %s`,
async (reloadDashboardsOnParamsChange, reloadOnParamsChange, withUid, editMode) => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
describe('reloadDashboardsOnParamsChange off', () => {
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(false, false, true, false));
it('without UID - no reload', () => runTest(false, false, false, false));
});
const dashboardScene = renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
describe('reloadOnParamsChange on', () => {
it('with UID - no reload', () => runTest(false, true, true, false));
it('without UID - no reload', () => runTest(false, true, false, false));
});
});
if (editMode) {
await enterEditMode(dashboardScene);
}
describe('reloadDashboardsOnParamsChange on', () => {
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(true, false, true, false));
it('without UID - no reload', () => runTest(true, false, false, false));
});
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
describe('reloadOnParamsChange on', () => {
describe('edit mode on', () => {
it('with UID - no reload', () => runTest(true, true, true, true));
it('without UID - no reload', () => runTest(true, true, false, true));
});
await updateTimeRange(dashboardScene);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
describe('edit mode off', () => {
it('with UID - reload', () => runTest(true, true, true, false));
it('without UID - no reload', () => runTest(true, true, false, false));
});
});
});
await updateMyVar(dashboardScene, '2');
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateScopes(['grafana']);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
getDashboardScenePageStateManager().clearDashboardCache();
getDashboardScenePageStateManager().clearSceneCache();
setDashboardAPI(undefined);
await resetScenes();
clearMocks();
}
);
});
+1 -3
View File
@@ -164,11 +164,9 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
public openExploreLink = async () => {
reportExploreMetrics('selected_metric_action_clicked', { action: 'open_in_explore' });
this.getLinkToExplore().then((link) => {
// We need to ensure we prefix with the appSubUrl for environments that don't host grafana at the root.
const url = `${config.appSubUrl}${link}`;
// We use window.open instead of a Link or <a> because we want to compute the explore link when clicking,
// if we precompute it we have to keep track of a lot of dependencies
window.open(url, '_blank');
window.open(link, '_blank');
});
};
@@ -22,7 +22,13 @@ export const Aggregation = (props: Props) => {
const selected = useSelectedFromOptions(aggOptions, props);
return (
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation">
<EditorField
label="Group by function"
data-testid="cloud-monitoring-aggregation"
tooltip={
'Aggregation function used on the metric data. Defaults to none for scalar data and mean for distribution data. Not applying an aggregation to distribution data may lead to performance issues.'
}
>
<Select
width="auto"
onChange={({ value }) => props.onChange(value!)}
@@ -9,7 +9,7 @@ import { getTemplateSrv } from '@grafana/runtime';
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
import { PreprocessorType, MetricKind } from '../types/query';
import { PreprocessorType, MetricKind, ValueTypes } from '../types/query';
import { defaultTimeSeriesList } from './MetricQueryEditor';
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
@@ -98,7 +98,11 @@ describe('VisualMetricQueryEditor', () => {
replace = (target?: string) => target || '';
const onChange = jest.fn();
const query = createMockTimeSeriesList();
const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' });
const mockMetricDescriptor = createMockMetricDescriptor({
displayName: 'metricName_test',
type: 'test_type',
valueType: ValueTypes.DOUBLE,
});
const datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
@@ -129,7 +133,56 @@ describe('VisualMetricQueryEditor', () => {
await select(metricName, 'metricName_test', { container: document.body });
});
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] })
expect.objectContaining({
filters: ['metric.type', '=', mockMetricDescriptor.type],
crossSeriesReducer: 'REDUCE_NONE',
})
);
});
it('can select a metric name with DISTRIBUTION valueType', async () => {
replace = (target?: string) => target || '';
const onChange = jest.fn();
const query = createMockTimeSeriesList();
const mockMetricDescriptor = createMockMetricDescriptor({
displayName: 'metricName_test',
type: 'test_type',
valueType: ValueTypes.DISTRIBUTION,
});
const datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
getLabels: jest.fn().mockResolvedValue([]),
});
const range = getDefaultTimeRange();
render(
<VisualMetricQueryEditor
{...defaultProps}
onChange={onChange}
datasource={datasource}
query={query}
range={range}
/>
);
const service = await screen.findByLabelText('Service');
openMenu(service);
await act(async () => {
await select(service, 'Srv', { container: document.body });
});
const metricName = await screen.findByLabelText('Metric name');
openMenu(metricName);
await userEvent.type(metricName, 'test');
await waitFor(() => expect(document.body).toHaveTextContent('metricName_test'));
await act(async () => {
await select(metricName, 'metricName_test', { container: document.body });
});
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
filters: ['metric.type', '=', mockMetricDescriptor.type],
crossSeriesReducer: 'REDUCE_MEAN',
})
);
});
@@ -209,6 +209,11 @@ export function Editor({
// On metric name change reset query to defaults except project name and filters
Object.assign(query, {
...defaultTimeSeriesList(datasource),
// If the metric value type is DISTRIBUTION use REDUCE_MEAN in order to avoid
// returning data frames with a large number of frames (as we return a frame per bucket).
// DISTRIBUTION metrics only typically make sense with an aggregation performed against them or
// when filtered to a specific label value.
crossSeriesReducer: valueType === ValueTypes.DISTRIBUTION ? 'REDUCE_MEAN' : 'REDUCE_NONE',
projectName: query.projectName,
filters: query.filters,
});
@@ -106,7 +106,8 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
{
projectName: this.templateSrv.replace(projectName),
groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}),
crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE',
// Use REDUCE_NONE to retrieve all available labels for the metric
crossSeriesReducer: 'REDUCE_NONE',
view: 'HEADERS',
},
this.templateSrv.replace(metricType)
@@ -43,12 +43,12 @@ composableKinds: PanelCfg: {
// Unit for the secondary stat to override what ever is set in the data frame.
secondaryStatUnit?: string
}
ZoomMode: "cooperative" | "greedy" @cuetsy(kind="enum")
ZoomMode: "cooperative" | "greedy" @cuetsy(kind="enum")
Options: {
nodes?: NodeOptions
edges?: EdgeOptions
// How to handle zoom/scroll events in the node graph
zoomMode?: ZoomMode
// How to handle zoom/scroll events in the node graph
zoomMode?: ZoomMode
} @cuetsy(kind="interface")
}
}]
@@ -45,7 +45,7 @@ composableKinds: PanelCfg: {
show?: XYShowMode & (*"points" | _)
pointSize?: {
pointSize?: {
fixed?: int32 & >=0
min?: int32 & >=0
max?: int32 & >=0
@@ -62,12 +62,12 @@ composableKinds: PanelCfg: {
} @cuetsy(kind="interface",TSVeneer="type")
XYSeriesConfig: {
name?: { fixed?: string }
frame?: { matcher: #MatcherConfig }
x?: { matcher: #MatcherConfig }
y?: { matcher: #MatcherConfig }
color?: { matcher: #MatcherConfig }
size?: { matcher: #MatcherConfig }
name?: {fixed?: string}
frame?: {matcher: #MatcherConfig}
x?: {matcher: #MatcherConfig}
y?: {matcher: #MatcherConfig}
color?: {matcher: #MatcherConfig}
size?: {matcher: #MatcherConfig}
} @cuetsy(kind="interface")
Options: {
+4
View File
@@ -280,6 +280,10 @@
"success": ""
}
},
"rule-list": {
"configure-datasource": "",
"new-alert-rule": ""
},
"rule-state": {
"creating": "",
"deleting": "",
+4
View File
@@ -280,6 +280,10 @@
"success": ""
}
},
"rule-list": {
"configure-datasource": "",
"new-alert-rule": ""
},
"rule-state": {
"creating": "",
"deleting": "",
+4
View File
@@ -280,6 +280,10 @@
"success": ""
}
},
"rule-list": {
"configure-datasource": "",
"new-alert-rule": ""
},
"rule-state": {
"creating": "",
"deleting": "",
+4
View File
@@ -280,6 +280,10 @@
"success": ""
}
},
"rule-list": {
"configure-datasource": "",
"new-alert-rule": ""
},
"rule-state": {
"creating": "",
"deleting": "",
+4
View File
@@ -276,6 +276,10 @@
"success": ""
}
},
"rule-list": {
"configure-datasource": "",
"new-alert-rule": ""
},
"rule-state": {
"creating": "",
"deleting": "",