mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-23 04:10:29 +01:00
Compare commits
4 Commits
nv/dashboa
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4b588235 | ||
|
|
ec003eb1e5 | ||
|
|
a98748bf92 | ||
|
|
a850ee34ec |
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -92,7 +93,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ user.Getter, _ serviceaccount.Getter, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
@@ -137,12 +138,12 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return authNs, nil
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, userGetter user.Getter, serviceAccountGetter serviceaccount.Getter, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, userGetter, serviceAccountGetter, dashboardModule), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
|
||||
@@ -407,3 +407,11 @@ cloudintegration:
|
||||
agent:
|
||||
# The version of the cloud integration agent.
|
||||
version: v0.0.8
|
||||
|
||||
##################### Authz #################################
|
||||
authz:
|
||||
# Specifies the authz provider to use.
|
||||
provider: openfga
|
||||
openfga:
|
||||
# maximum tuples allowed per openfga write operation.
|
||||
max_tuples_per_write: 100
|
||||
|
||||
@@ -1035,18 +1035,6 @@ components:
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
CommonDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
CommonJSONRef:
|
||||
properties:
|
||||
$ref:
|
||||
type: string
|
||||
type: object
|
||||
ConfigAuthorization:
|
||||
properties:
|
||||
credentials:
|
||||
@@ -1998,43 +1986,6 @@ components:
|
||||
to_user:
|
||||
type: string
|
||||
type: object
|
||||
DashboardGridItem:
|
||||
properties:
|
||||
content:
|
||||
$ref: '#/components/schemas/CommonJSONRef'
|
||||
height:
|
||||
type: integer
|
||||
width:
|
||||
type: integer
|
||||
x:
|
||||
type: integer
|
||||
"y":
|
||||
type: integer
|
||||
type: object
|
||||
DashboardGridLayoutCollapse:
|
||||
properties:
|
||||
open:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardGridLayoutDisplay:
|
||||
properties:
|
||||
collapse:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutCollapse'
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
DashboardGridLayoutSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutDisplay'
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardGridItem'
|
||||
nullable: true
|
||||
type: array
|
||||
repeatVariable:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -2089,749 +2040,6 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2Axes:
|
||||
properties:
|
||||
isLogScale:
|
||||
type: boolean
|
||||
softMax:
|
||||
nullable: true
|
||||
type: number
|
||||
softMin:
|
||||
nullable: true
|
||||
type: number
|
||||
type: object
|
||||
Dashboardtypesv2BarChartPanelSpec:
|
||||
properties:
|
||||
axes:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Axes'
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdWithLabel'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BarChartVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2BarChartVisualization:
|
||||
properties:
|
||||
fillSpans:
|
||||
type: boolean
|
||||
stackedBarChart:
|
||||
type: boolean
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2BasicVisualization:
|
||||
properties:
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2BuilderQuerySpec:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
Dashboardtypesv2ComparisonOperator:
|
||||
enum:
|
||||
- '>'
|
||||
- <
|
||||
- '>='
|
||||
- <=
|
||||
- =
|
||||
- above
|
||||
- below
|
||||
- above_or_equal
|
||||
- below_or_equal
|
||||
- equal
|
||||
- not_equal
|
||||
type: string
|
||||
Dashboardtypesv2ComparisonThreshold:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
format:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdFormat'
|
||||
operator:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonOperator'
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
type: object
|
||||
Dashboardtypesv2CustomVariableSpec:
|
||||
properties:
|
||||
customValue:
|
||||
type: string
|
||||
required:
|
||||
- customValue
|
||||
type: object
|
||||
Dashboardtypesv2DashboardData:
|
||||
properties:
|
||||
datasources:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DatasourceSpec'
|
||||
type: object
|
||||
display:
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
duration:
|
||||
type: string
|
||||
layouts:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Layout'
|
||||
nullable: true
|
||||
type: array
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
panels:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Panel'
|
||||
nullable: true
|
||||
type: object
|
||||
refreshInterval:
|
||||
type: string
|
||||
variables:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Variable'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2DatasourcePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2DatasourcePluginVariantStruct'
|
||||
Dashboardtypesv2DatasourcePluginKind:
|
||||
enum:
|
||||
- signoz/Datasource
|
||||
type: string
|
||||
Dashboardtypesv2DatasourcePluginVariantStruct:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/Datasource
|
||||
type: string
|
||||
spec:
|
||||
nullable: true
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2DatasourceSpec:
|
||||
properties:
|
||||
default:
|
||||
type: boolean
|
||||
display:
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DatasourcePlugin'
|
||||
type: object
|
||||
Dashboardtypesv2DynamicVariableSpec:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
signal:
|
||||
$ref: '#/components/schemas/TelemetrytypesSignal'
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
Dashboardtypesv2FillMode:
|
||||
enum:
|
||||
- solid
|
||||
- gradient
|
||||
- none
|
||||
type: string
|
||||
Dashboardtypesv2HistogramBuckets:
|
||||
properties:
|
||||
bucketCount:
|
||||
nullable: true
|
||||
type: number
|
||||
bucketWidth:
|
||||
nullable: true
|
||||
type: number
|
||||
mergeAllActiveQueries:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2HistogramPanelSpec:
|
||||
properties:
|
||||
histogramBuckets:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2HistogramBuckets'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
type: object
|
||||
Dashboardtypesv2Layout:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2LayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
|
||||
Dashboardtypesv2LayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- Grid
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardGridLayoutSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2Legend:
|
||||
properties:
|
||||
customColors:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
position:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LegendPosition'
|
||||
type: object
|
||||
Dashboardtypesv2LegendPosition:
|
||||
enum:
|
||||
- bottom
|
||||
- right
|
||||
type: string
|
||||
Dashboardtypesv2LineInterpolation:
|
||||
enum:
|
||||
- linear
|
||||
- spline
|
||||
- step_after
|
||||
- step_before
|
||||
type: string
|
||||
Dashboardtypesv2LineStyle:
|
||||
enum:
|
||||
- solid
|
||||
- dashed
|
||||
type: string
|
||||
Dashboardtypesv2ListPanelSpec:
|
||||
properties:
|
||||
selectFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2ListVariableSpec:
|
||||
properties:
|
||||
allowAllValue:
|
||||
type: boolean
|
||||
allowMultiple:
|
||||
type: boolean
|
||||
capturingRegexp:
|
||||
type: string
|
||||
customAllValue:
|
||||
type: string
|
||||
defaultValue:
|
||||
$ref: '#/components/schemas/VariableDefaultValue'
|
||||
display:
|
||||
$ref: '#/components/schemas/VariableDisplay'
|
||||
name:
|
||||
type: string
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2VariablePlugin'
|
||||
sort:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2NumberPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonThreshold'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2Panel:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelSpec'
|
||||
type: object
|
||||
Dashboardtypesv2PanelFormatting:
|
||||
properties:
|
||||
decimalPrecision:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PrecisionOption'
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2PanelPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TimeSeriesPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BarChartPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2NumberPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2PieChartPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TablePanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2HistogramPanelSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListPanelSpec'
|
||||
Dashboardtypesv2PanelPluginKind:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
- signoz/BarChartPanel
|
||||
- signoz/NumberPanel
|
||||
- signoz/PieChartPanel
|
||||
- signoz/TablePanel
|
||||
- signoz/HistogramPanel
|
||||
- signoz/ListPanel
|
||||
type: string
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BarChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/BarChartPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BarChartPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2HistogramPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/HistogramPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2HistogramPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/ListPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ListPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2NumberPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/NumberPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2NumberPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2PieChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/PieChartPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PieChartPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TablePanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TablePanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TablePanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TimeSeriesPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2PanelSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/V1PanelDisplay'
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelPlugin'
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Query'
|
||||
type: array
|
||||
type: object
|
||||
Dashboardtypesv2PieChartPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2PrecisionOption:
|
||||
enum:
|
||||
- "0"
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
- "4"
|
||||
- full
|
||||
type: string
|
||||
Dashboardtypesv2Query:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QuerySpec'
|
||||
type: object
|
||||
Dashboardtypesv2QueryPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BuilderQuerySpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator'
|
||||
Dashboardtypesv2QueryPluginKind:
|
||||
enum:
|
||||
- signoz/BuilderQuery
|
||||
- signoz/CompositeQuery
|
||||
- signoz/Formula
|
||||
- signoz/PromQLQuery
|
||||
- signoz/ClickHouseSQL
|
||||
- signoz/TraceOperator
|
||||
type: string
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2BuilderQuerySpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/BuilderQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BuilderQuerySpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/ClickHouseSQL
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/CompositeQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5CompositeQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/PromQLQuery
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/Formula
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TraceOperator
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2QuerySpec:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
plugin:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QueryPlugin'
|
||||
type: object
|
||||
Dashboardtypesv2QueryVariableSpec:
|
||||
properties:
|
||||
queryValue:
|
||||
type: string
|
||||
required:
|
||||
- queryValue
|
||||
type: object
|
||||
Dashboardtypesv2SpanGaps:
|
||||
properties:
|
||||
fillLessThan:
|
||||
type: string
|
||||
fillOnlyBelow:
|
||||
type: boolean
|
||||
type: object
|
||||
Dashboardtypesv2TableFormatting:
|
||||
properties:
|
||||
columnUnits:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
decimalPrecision:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PrecisionOption'
|
||||
type: object
|
||||
Dashboardtypesv2TablePanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TableFormatting'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TableThreshold'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2BasicVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2TableThreshold:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
columnName:
|
||||
type: string
|
||||
format:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdFormat'
|
||||
operator:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ComparisonOperator'
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
- columnName
|
||||
type: object
|
||||
Dashboardtypesv2TextVariableSpec:
|
||||
properties:
|
||||
constant:
|
||||
type: boolean
|
||||
display:
|
||||
$ref: '#/components/schemas/VariableDisplay'
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
Dashboardtypesv2TextboxVariableSpec:
|
||||
type: object
|
||||
Dashboardtypesv2ThresholdFormat:
|
||||
enum:
|
||||
- text
|
||||
- background
|
||||
type: string
|
||||
Dashboardtypesv2ThresholdWithLabel:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
- color
|
||||
- label
|
||||
type: object
|
||||
Dashboardtypesv2TimePreference:
|
||||
enum:
|
||||
- global_time
|
||||
- last_5_min
|
||||
- last_15_min
|
||||
- last_30_min
|
||||
- last_1_hr
|
||||
- last_6_hr
|
||||
- last_1_day
|
||||
- last_3_days
|
||||
- last_1_week
|
||||
- last_1_month
|
||||
type: string
|
||||
Dashboardtypesv2TimeSeriesChartAppearance:
|
||||
properties:
|
||||
fillMode:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2FillMode'
|
||||
lineInterpolation:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LineInterpolation'
|
||||
lineStyle:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2LineStyle'
|
||||
showPoints:
|
||||
type: boolean
|
||||
spanGaps:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2SpanGaps'
|
||||
type: object
|
||||
Dashboardtypesv2TimeSeriesPanelSpec:
|
||||
properties:
|
||||
axes:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Axes'
|
||||
chartAppearance:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesChartAppearance'
|
||||
formatting:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2PanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2Legend'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ThresholdWithLabel'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimeSeriesVisualization'
|
||||
type: object
|
||||
Dashboardtypesv2TimeSeriesVisualization:
|
||||
properties:
|
||||
fillSpans:
|
||||
type: boolean
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TimePreference'
|
||||
type: object
|
||||
Dashboardtypesv2Variable:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextVariableSpec'
|
||||
Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2ListVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- ListVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2ListVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- TextVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TextVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2DynamicVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2QueryVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2CustomVariableSpec'
|
||||
- $ref: '#/components/schemas/Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextboxVariableSpec'
|
||||
Dashboardtypesv2VariablePluginKind:
|
||||
enum:
|
||||
- signoz/DynamicVariable
|
||||
- signoz/QueryVariable
|
||||
- signoz/CustomVariable
|
||||
- signoz/TextboxVariable
|
||||
type: string
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2CustomVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/CustomVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2CustomVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2DynamicVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/DynamicVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DynamicVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2QueryVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/QueryVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2QueryVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
Dashboardtypesv2VariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDashboardtypesv2TextboxVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TextboxVariable
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2TextboxVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
ErrorsJSON:
|
||||
properties:
|
||||
code:
|
||||
@@ -5343,37 +4551,6 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
V1Link:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
V1PanelDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
VariableDefaultValue:
|
||||
type: object
|
||||
VariableDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
hidden:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
ZeustypesGettableHost:
|
||||
properties:
|
||||
hosts:
|
||||
@@ -10168,58 +9345,6 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/dashboards:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'TEMP: dummy endpoint to exercise v2 spec shape while we iterate
|
||||
on plugin schema.'
|
||||
operationId: CreateDashboardV2
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DashboardData'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Dashboardtypesv2DashboardData'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create dashboard v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -20,20 +22,24 @@ import (
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
pkgAuthzService authz.AuthZ
|
||||
openfgaServer *openfgaserver.Server
|
||||
licensing licensing.Licensing
|
||||
store authtypes.RoleStore
|
||||
registry []authz.RegisterTypeable
|
||||
config authz.Config
|
||||
pkgAuthzService authz.AuthZ
|
||||
openfgaServer *openfgaserver.Server
|
||||
licensing licensing.Licensing
|
||||
store authtypes.RoleStore
|
||||
registry []authz.RegisterTypeable
|
||||
settings factory.ScopedProviderSettings
|
||||
userGetter user.Getter
|
||||
serviceAccountGetter serviceaccount.Getter
|
||||
}
|
||||
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, userGetter user.Getter, serviceAccountGetter serviceaccount.Getter, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, registry)
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, userGetter, serviceAccountGetter, registry)
|
||||
})
|
||||
}
|
||||
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, userGetter user.Getter, serviceAccountGetter serviceaccount.Getter, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
|
||||
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
@@ -45,12 +51,18 @@ func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopedSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/authz/openfgaauthz")
|
||||
|
||||
return &provider{
|
||||
pkgAuthzService: pkgAuthzService,
|
||||
openfgaServer: openfgaServer,
|
||||
licensing: licensing,
|
||||
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
|
||||
registry: registry,
|
||||
config: config,
|
||||
pkgAuthzService: pkgAuthzService,
|
||||
openfgaServer: openfgaServer,
|
||||
licensing: licensing,
|
||||
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
|
||||
registry: registry,
|
||||
settings: scopedSettings,
|
||||
userGetter: userGetter,
|
||||
serviceAccountGetter: serviceAccountGetter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -78,14 +90,18 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
|
||||
return provider.openfgaServer.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, typeable)
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return provider.openfgaServer.Write(ctx, additions, deletions)
|
||||
}
|
||||
|
||||
func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error) {
|
||||
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
|
||||
return provider.pkgAuthzService.Get(ctx, orgID, id)
|
||||
}
|
||||
@@ -146,7 +162,7 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
|
||||
return provider.store.Create(ctx, role)
|
||||
}
|
||||
|
||||
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
|
||||
@@ -163,10 +179,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
|
||||
}
|
||||
|
||||
if existingRole != nil {
|
||||
return authtypes.NewRoleFromStorableRole(existingRole), nil
|
||||
return existingRole, nil
|
||||
}
|
||||
|
||||
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
|
||||
err = provider.store.Create(ctx, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,14 +191,13 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
|
||||
}
|
||||
|
||||
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
|
||||
typeables := make([]authtypes.Typeable, 0)
|
||||
for _, register := range provider.registry {
|
||||
typeables = append(typeables, register.MustGetTypeables()...)
|
||||
}
|
||||
|
||||
typeables = append(typeables, provider.MustGetTypeables()...)
|
||||
resources := make([]*authtypes.Resource, 0)
|
||||
for _, typeable := range typeables {
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
|
||||
@@ -201,21 +216,23 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
|
||||
}
|
||||
|
||||
objects := make([]*authtypes.Object, 0)
|
||||
for _, resource := range provider.GetResources(ctx) {
|
||||
if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) {
|
||||
resourceObjects, err := provider.
|
||||
ListObjects(
|
||||
ctx,
|
||||
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
|
||||
relation,
|
||||
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects = append(objects, resourceObjects...)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceObjects, err := provider.
|
||||
ListObjects(
|
||||
ctx,
|
||||
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
|
||||
relation,
|
||||
objectType,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects = append(objects, resourceObjects...)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
@@ -227,7 +244,7 @@ func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *au
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
|
||||
return provider.store.Update(ctx, orgID, role)
|
||||
}
|
||||
|
||||
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
@@ -260,18 +277,43 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableRole, err := provider.store.Get(ctx, orgID, id)
|
||||
role, err := provider.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role := authtypes.NewRoleFromStorableRole(storableRole)
|
||||
err = role.ErrIfManaged()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return provider.store.Delete(ctx, orgID, id)
|
||||
users, err := provider.userGetter.GetUsersByOrgIDAndRoleID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// cannot move these checks in type layer until the coretypes are sorted
|
||||
if len(users) > 0 {
|
||||
return errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleHasUserAssignees, "role has active user assignments, remove them before deleting")
|
||||
}
|
||||
|
||||
serviceAccounts, err := provider.serviceAccountGetter.GetServiceAccountsByOrgIDAndRoleID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(serviceAccounts) > 0 {
|
||||
return errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleHasServiceAccountAssignees, "role has active service account assignments, remove them before deleting")
|
||||
}
|
||||
|
||||
if err := provider.deleteTuples(ctx, role.Name, orgID); err != nil {
|
||||
return errors.WithAdditionalf(err, "failed to delete transactions for the role: %s", role.Name)
|
||||
}
|
||||
|
||||
err = provider.store.Delete(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
||||
@@ -346,3 +388,62 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
|
||||
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
|
||||
User: subject,
|
||||
Object: objectType.StringValue() + ":",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tuples = append(tuples, typeTuples...)
|
||||
}
|
||||
|
||||
if len(tuples) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(tuples); idx += provider.config.OpenFGA.MaxTuplesPerWrite {
|
||||
end := idx + provider.config.OpenFGA.MaxTuplesPerWrite
|
||||
if end > len(tuples) {
|
||||
end = len(tuples)
|
||||
}
|
||||
|
||||
err := provider.Write(ctx, nil, tuples[idx:end])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) getUniqueTypes() []authtypes.Type {
|
||||
seen := make(map[string]struct{})
|
||||
uniqueTypes := make([]authtypes.Type, 0)
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
|
||||
return uniqueTypes
|
||||
}
|
||||
|
||||
@@ -110,10 +110,14 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
|
||||
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
return server.pkgAuthzService.ListObjects(ctx, subject, relation, typeable)
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return server.pkgAuthzService.Write(ctx, additions, deletions)
|
||||
}
|
||||
|
||||
func (server *Server) ReadTuples(ctx context.Context, tupleKey *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error) {
|
||||
return server.pkgAuthzService.ReadTuples(ctx, tupleKey)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes/dashboardtypesv2"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -138,25 +137,5 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TEMP dummy route to exercise the dashboard v2 schema in OpenAPI generation.
|
||||
// Handler function is not invoked at spec-gen time; only OpenAPIDef types are
|
||||
// walked. Reuses provider.dashboardHandler.Create to avoid wiring a real handler.
|
||||
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create dashboard v2",
|
||||
Description: "TEMP: dummy endpoint to exercise v2 spec shape while we iterate on plugin schema.",
|
||||
Request: new(dashboardtypesv2.DashboardData),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypesv2.DashboardData),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type AuthZ interface {
|
||||
Write(context.Context, []*openfgav1.TupleKey, []*openfgav1.TupleKey) error
|
||||
|
||||
// Lists the selectors for objects assigned to subject (s) with relation (r) on resource (s)
|
||||
ListObjects(context.Context, string, authtypes.Relation, authtypes.Typeable) ([]*authtypes.Object, error)
|
||||
ListObjects(context.Context, string, authtypes.Relation, authtypes.Type) ([]*authtypes.Object, error)
|
||||
|
||||
// Creates the role.
|
||||
Create(context.Context, valuer.UUID, *authtypes.Role) error
|
||||
@@ -78,6 +78,9 @@ type AuthZ interface {
|
||||
|
||||
// Bootstrap managed roles transactions and user assignments
|
||||
CreateManagedUserRoleTransactions(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
// ReadTuples reads tuples from the authorization server matching the given tuple key filter.
|
||||
ReadTuples(context.Context, *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error)
|
||||
}
|
||||
|
||||
type RegisterTypeable interface {
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) authtypes.RoleStore {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) error {
|
||||
func (store *store) Create(ctx context.Context, role *authtypes.Role) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -32,8 +32,8 @@ func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.StorableRole, error) {
|
||||
role := new(authtypes.StorableRole)
|
||||
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
|
||||
role := new(authtypes.Role)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -49,8 +49,8 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.StorableRole, error) {
|
||||
role := new(authtypes.StorableRole)
|
||||
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
|
||||
role := new(authtypes.Role)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -66,8 +66,8 @@ func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, na
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.StorableRole, error) {
|
||||
roles := make([]*authtypes.StorableRole, 0)
|
||||
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
|
||||
roles := make([]*authtypes.Role, 0)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -82,8 +82,8 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.S
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.StorableRole, error) {
|
||||
roles := make([]*authtypes.StorableRole, 0)
|
||||
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
|
||||
roles := make([]*authtypes.Role, 0)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -103,8 +103,8 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.StorableRole, error) {
|
||||
roles := make([]*authtypes.StorableRole, 0)
|
||||
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
|
||||
roles := make([]*authtypes.Role, 0)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -124,7 +124,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.StorableRole) error {
|
||||
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -145,7 +145,7 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(authtypes.StorableRole)).
|
||||
Model(new(authtypes.Role)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
|
||||
@@ -4,14 +4,30 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct{}
|
||||
type Config struct {
|
||||
// Provider is the name of the authorization provider to use.
|
||||
Provider string `mapstructure:"provider"`
|
||||
|
||||
// OpenFGA is the configuration specific to the OpenFGA authorization provider.
|
||||
OpenFGA OpenFGAConfig `mapstructure:"openfga"`
|
||||
}
|
||||
|
||||
type OpenFGAConfig struct {
|
||||
// MaxTuplesPerWrite is the maximum number of tuples to include in a single write call.
|
||||
MaxTuplesPerWrite int `mapstructure:"max_tuples_per_write"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("authz"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{}
|
||||
return &Config{
|
||||
Provider: "openfga",
|
||||
OpenFGA: OpenFGAConfig{
|
||||
MaxTuplesPerWrite: 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
|
||||
@@ -68,68 +68,32 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
|
||||
return provider.server.Write(ctx, additions, deletions)
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
return provider.server.ListObjects(ctx, subject, relation, typeable)
|
||||
func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error) {
|
||||
return provider.server.ReadTuples(ctx, tupleKey)
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
return provider.server.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
|
||||
storableRole, err := provider.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authtypes.NewRoleFromStorableRole(storableRole), nil
|
||||
return provider.store.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
|
||||
storableRole, err := provider.store.GetByOrgIDAndName(ctx, orgID, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authtypes.NewRoleFromStorableRole(storableRole), nil
|
||||
return provider.store.GetByOrgIDAndName(ctx, orgID, name)
|
||||
}
|
||||
|
||||
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
|
||||
storableRoles, err := provider.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*authtypes.Role, len(storableRoles))
|
||||
for idx, storableRole := range storableRoles {
|
||||
roles[idx] = authtypes.NewRoleFromStorableRole(storableRole)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
return provider.store.List(ctx, orgID)
|
||||
}
|
||||
|
||||
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
|
||||
storableRoles, err := provider.store.ListByOrgIDAndNames(ctx, orgID, names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*authtypes.Role, len(storableRoles))
|
||||
for idx, storable := range storableRoles {
|
||||
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
return provider.store.ListByOrgIDAndNames(ctx, orgID, names)
|
||||
}
|
||||
|
||||
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
|
||||
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]*authtypes.Role, len(storableRoles))
|
||||
for idx, storable := range storableRoles {
|
||||
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
return provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
|
||||
}
|
||||
|
||||
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
|
||||
@@ -197,7 +161,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
|
||||
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
|
||||
err := provider.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
for _, role := range managedRoles {
|
||||
err := provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
|
||||
err := provider.store.Create(ctx, role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -265,17 +265,45 @@ func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
|
||||
func (server *Server) ReadTuples(ctx context.Context, tupleKey *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error) {
|
||||
storeID, _ := server.getStoreIDandModelID()
|
||||
var tuples []*openfgav1.TupleKey
|
||||
continuationToken := ""
|
||||
|
||||
for {
|
||||
response, err := server.openfgaServer.Read(ctx, &openfgav1.ReadRequest{
|
||||
StoreId: storeID,
|
||||
TupleKey: tupleKey,
|
||||
ContinuationToken: continuationToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "failed to read tuples from authorization server")
|
||||
}
|
||||
|
||||
for _, tuple := range response.Tuples {
|
||||
tuples = append(tuples, tuple.Key)
|
||||
}
|
||||
|
||||
if response.ContinuationToken == "" {
|
||||
break
|
||||
}
|
||||
continuationToken = response.ContinuationToken
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
storeID, modelID := server.getStoreIDandModelID()
|
||||
response, err := server.openfgaServer.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: storeID,
|
||||
AuthorizationModelId: modelID,
|
||||
User: subject,
|
||||
Relation: relation.StringValue(),
|
||||
Type: typeable.Type().StringValue(),
|
||||
Type: objectType.StringValue(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "cannot list objects for subject %s with relation %s for type %s", subject, relation.StringValue(), typeable.Type().StringValue())
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "cannot list objects for subject %s with relation %s for type %s", subject, relation.StringValue(), objectType.StringValue())
|
||||
}
|
||||
|
||||
return authtypes.MustNewObjectsFromStringSlice(response.Objects), nil
|
||||
|
||||
21
pkg/modules/serviceaccount/implserviceaccount/getter.go
Normal file
21
pkg/modules/serviceaccount/implserviceaccount/getter.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package implserviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type getter struct {
|
||||
store serviceaccounttypes.Store
|
||||
}
|
||||
|
||||
func NewGetter(store serviceaccounttypes.Store) serviceaccount.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (getter *getter) GetServiceAccountsByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*serviceaccounttypes.ServiceAccount, error) {
|
||||
return getter.store.GetServiceAccountsByOrgIDAndRoleID(ctx, orgID, roleID)
|
||||
}
|
||||
@@ -123,6 +123,25 @@ func (store *store) GetByIDAndStatus(ctx context.Context, id valuer.UUID, status
|
||||
return storable, nil
|
||||
}
|
||||
|
||||
func (store *store) GetServiceAccountsByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*serviceaccounttypes.ServiceAccount, error) {
|
||||
serviceAccounts := make([]*serviceaccounttypes.ServiceAccount, 0)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&serviceAccounts).
|
||||
Join(`JOIN service_account_role ON service_account_role.service_account_id = service_account.id`).
|
||||
Where(`service_account.org_id = ?`, orgID).
|
||||
Where("service_account_role.role_id = ?", roleID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serviceAccounts, nil
|
||||
}
|
||||
|
||||
func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
||||
storable := new(serviceaccounttypes.ServiceAccount)
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Getter interface {
|
||||
GetServiceAccountsByOrgIDAndRoleID(context.Context, valuer.UUID, valuer.UUID) ([]*serviceaccounttypes.ServiceAccount, error)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
// Creates a new service account for an organization.
|
||||
Create(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
@@ -131,6 +132,9 @@ type Config struct {
|
||||
|
||||
// CloudIntegration config
|
||||
CloudIntegration cloudintegration.Config `mapstructure:"cloudintegration"`
|
||||
|
||||
// Authz config
|
||||
Authz authz.Config `mapstructure:"authz"`
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.ResolverConfig) (Config, error) {
|
||||
@@ -163,6 +167,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
serviceaccount.NewConfigFactory(),
|
||||
auditor.NewConfigFactory(),
|
||||
cloudintegration.NewConfigFactory(),
|
||||
authz.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -100,7 +101,7 @@ func New(
|
||||
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
|
||||
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
|
||||
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, user.Getter, serviceaccount.Getter, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
|
||||
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
@@ -328,19 +329,21 @@ func New(
|
||||
// Initialize dashboard module (needed for authz registry)
|
||||
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
|
||||
// Initialize authz
|
||||
authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize service account getter
|
||||
serviceAccountGetter := implserviceaccount.NewGetter(implserviceaccount.NewStore(sqlstore))
|
||||
// Initialize user getter
|
||||
userGetter := impluser.NewGetter(userStore, userRoleStore, flagger)
|
||||
|
||||
// Initialize authz
|
||||
authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, userGetter, serviceAccountGetter, dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authz, err := authzProviderFactory.New(ctx, providerSettings, config.Authz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize notification manager from the available notification manager provider factories
|
||||
nfManager, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
|
||||
@@ -54,7 +54,7 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
managedRoles := []*authtypes.StorableRole{}
|
||||
managedRoles := []*authtypes.Role{}
|
||||
for _, orgIDStr := range orgIDs {
|
||||
orgID, err := valuer.NewUUID(orgIDStr)
|
||||
if err != nil {
|
||||
@@ -63,19 +63,19 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
// signoz admin
|
||||
signozAdminRole := authtypes.NewRole(authtypes.SigNozAdminRoleName, authtypes.SigNozAdminRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAdminRole))
|
||||
managedRoles = append(managedRoles, signozAdminRole)
|
||||
|
||||
// signoz editor
|
||||
signozEditorRole := authtypes.NewRole(authtypes.SigNozEditorRoleName, authtypes.SigNozEditorRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozEditorRole))
|
||||
managedRoles = append(managedRoles, signozEditorRole)
|
||||
|
||||
// signoz viewer
|
||||
signozViewerRole := authtypes.NewRole(authtypes.SigNozViewerRoleName, authtypes.SigNozViewerRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozViewerRole))
|
||||
managedRoles = append(managedRoles, signozViewerRole)
|
||||
|
||||
// signoz anonymous
|
||||
signozAnonymousRole := authtypes.NewRole(authtypes.SigNozAnonymousRoleName, authtypes.SigNozAnonymousRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAnonymousRole))
|
||||
managedRoles = append(managedRoles, signozAnonymousRole)
|
||||
}
|
||||
|
||||
if len(managedRoles) > 0 {
|
||||
|
||||
@@ -20,6 +20,8 @@ var (
|
||||
ErrCodeRoleNotFound = errors.MustNewCode("role_not_found")
|
||||
ErrCodeRoleFailedTransactionsFromString = errors.MustNewCode("role_failed_transactions_from_string")
|
||||
ErrCodeRoleUnsupported = errors.MustNewCode("role_unsupported")
|
||||
ErrCodeRoleHasUserAssignees = errors.MustNewCode("role_has_user_assignees")
|
||||
ErrCodeRoleHasServiceAccountAssignees = errors.MustNewCode("role_has_service_account_assignees")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -60,17 +62,6 @@ var (
|
||||
TypeableResourcesRoles = MustNewTypeableMetaResources(MustNewName("roles"))
|
||||
)
|
||||
|
||||
type StorableRole struct {
|
||||
bun.BaseModel `bun:"table:role"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `bun:"name,type:string" json:"name"`
|
||||
Description string `bun:"description,type:string" json:"description"`
|
||||
Type string `bun:"type,type:string" json:"type"`
|
||||
OrgID string `bun:"org_id,type:string" json:"orgId"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
bun.BaseModel `bun:"table:role"`
|
||||
|
||||
@@ -91,28 +82,6 @@ type PatchableRole struct {
|
||||
Description string `json:"description" required:"true"`
|
||||
}
|
||||
|
||||
func NewStorableRoleFromRole(role *Role) *StorableRole {
|
||||
return &StorableRole{
|
||||
Identifiable: role.Identifiable,
|
||||
TimeAuditable: role.TimeAuditable,
|
||||
Name: role.Name,
|
||||
Description: role.Description,
|
||||
Type: role.Type.String(),
|
||||
OrgID: role.OrgID.StringValue(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRoleFromStorableRole(storableRole *StorableRole) *Role {
|
||||
return &Role{
|
||||
Identifiable: storableRole.Identifiable,
|
||||
TimeAuditable: storableRole.TimeAuditable,
|
||||
Name: storableRole.Name,
|
||||
Description: storableRole.Description,
|
||||
Type: valuer.NewString(storableRole.Type),
|
||||
OrgID: valuer.MustNewUUID(storableRole.OrgID),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID) *Role {
|
||||
return &Role{
|
||||
Identifiable: types.Identifiable{
|
||||
@@ -264,13 +233,13 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
|
||||
}
|
||||
|
||||
type RoleStore interface {
|
||||
Create(context.Context, *StorableRole) error
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
|
||||
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
|
||||
List(context.Context, valuer.UUID) ([]*StorableRole, error)
|
||||
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
|
||||
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
|
||||
Update(context.Context, valuer.UUID, *StorableRole) error
|
||||
Create(context.Context, *Role) error
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*Role, error)
|
||||
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*Role, error)
|
||||
List(context.Context, valuer.UUID) ([]*Role, error)
|
||||
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*Role, error)
|
||||
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*Role, error)
|
||||
Update(context.Context, valuer.UUID, *Role) error
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
RunInTx(context.Context, func(ctx context.Context) error) error
|
||||
}
|
||||
|
||||
262
pkg/types/dashboardtypes/dashboard_v2.go
Normal file
262
pkg/types/dashboardtypes/dashboard_v2.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/go-playground/validator/v10"
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/common"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
)
|
||||
|
||||
// StorableDashboardDataV2 wraps v1.DashboardSpec (Perses) with additional SigNoz-specific fields.
|
||||
//
|
||||
// We embed DashboardSpec (not v1.Dashboard) to avoid carrying Perses's Metadata
|
||||
// (Name, Project, CreatedAt, UpdatedAt, Tags, Version) and Kind field. SigNoz
|
||||
// manages identity (ID), timestamps (TimeAuditable), and multi-tenancy (OrgID)
|
||||
// separately on StorableDashboardV2/DashboardV2.
|
||||
//
|
||||
// The following v1 request fields map to locations inside v1.DashboardSpec:
|
||||
// - title → Display.Name (common.Display)
|
||||
// - description → Display.Description (common.Display)
|
||||
//
|
||||
// Fields that have no Perses equivalent will be added in this wrapper (like image, uploadGrafana, etc.)
|
||||
type StorableDashboardDataV2 = v1.DashboardSpec
|
||||
|
||||
// UnmarshalAndValidateDashboardV2JSON unmarshals the JSON into a StorableDashboardDataV2
|
||||
// (= PostableDashboardV2 = UpdatableDashboardV2) and validates plugin kinds and specs.
|
||||
func UnmarshalAndValidateDashboardV2JSON(data []byte) (*StorableDashboardDataV2, error) {
|
||||
var d StorableDashboardDataV2
|
||||
// Note: DashboardSpec has a custom UnmarshalJSON which prevents
|
||||
// DisallowUnknownFields from working at the top level. Unknown
|
||||
// fields in plugin specs are still rejected by validateAndNormalizePluginSpec.
|
||||
if err := json.Unmarshal(data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDashboardV2(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// Plugin kind → spec type factory. Each value is a pointer to the zero value of the
|
||||
// expected spec struct. validatePluginSpec marshals plugin.Spec back to JSON and
|
||||
// unmarshals into the typed struct to catch field-level errors.
|
||||
var (
|
||||
panelPluginSpecs = map[PanelPluginKind]func() any{
|
||||
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
|
||||
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
|
||||
PanelKindNumber: func() any { return new(NumberPanelSpec) },
|
||||
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
|
||||
PanelKindList: func() any { return new(ListPanelSpec) },
|
||||
}
|
||||
queryPluginSpecs = map[QueryPluginKind]func() any{
|
||||
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
|
||||
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
|
||||
QueryKindFormula: func() any { return new(FormulaSpec) },
|
||||
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
|
||||
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
|
||||
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
|
||||
}
|
||||
variablePluginSpecs = map[VariablePluginKind]func() any{
|
||||
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
|
||||
VariableKindQuery: func() any { return new(QueryVariableSpec) },
|
||||
VariableKindCustom: func() any { return new(CustomVariableSpec) },
|
||||
VariableKindTextbox: func() any { return new(TextboxVariableSpec) },
|
||||
}
|
||||
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
|
||||
DatasourceKindSigNoz: func() any { return new(struct{}) },
|
||||
}
|
||||
|
||||
// allowedQueryKinds maps each panel plugin kind to the query plugin
|
||||
// kinds it supports. Composite sub-query types are mapped to these
|
||||
// same kind strings via compositeSubQueryTypeToPluginKind.
|
||||
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
|
||||
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindList: {QueryKindBuilder},
|
||||
}
|
||||
|
||||
// compositeSubQueryTypeToPluginKind maps CompositeQuery sub-query type
|
||||
// strings to the equivalent top-level query plugin kind for validation.
|
||||
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
|
||||
qb.QueryTypeBuilder: QueryKindBuilder,
|
||||
qb.QueryTypeFormula: QueryKindFormula,
|
||||
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
|
||||
qb.QueryTypePromQL: QueryKindPromQL,
|
||||
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
|
||||
}
|
||||
)
|
||||
|
||||
func validateDashboardV2(d StorableDashboardDataV2) error {
|
||||
for name, ds := range d.Datasources {
|
||||
if err := validateDatasourcePlugin(&ds.Plugin, fmt.Sprintf("spec.datasources.%s.plugin", name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, v := range d.Variables {
|
||||
if err := validateVariablePlugin(v, fmt.Sprintf("spec.variables[%d]", i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for key, panel := range d.Panels {
|
||||
if panel == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
|
||||
}
|
||||
path := fmt.Sprintf("spec.panels.%s", key)
|
||||
if err := validatePanelPlugin(&panel.Spec.Plugin, path+".spec.plugin"); err != nil {
|
||||
return err
|
||||
}
|
||||
panelKind := PanelPluginKind(panel.Spec.Plugin.Kind)
|
||||
allowed := allowedQueryKinds[panelKind]
|
||||
for qi := range panel.Spec.Queries {
|
||||
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
|
||||
if err := validateQueryPlugin(&panel.Spec.Queries[qi].Spec.Plugin, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateQueryAllowedForPanel(panel.Spec.Queries[qi].Spec.Plugin, allowed, panelKind, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDatasourcePlugin(plugin *common.Plugin, path string) error {
|
||||
kind := DatasourcePluginKind(plugin.Kind)
|
||||
factory, ok := datasourcePluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown datasource plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func validateVariablePlugin(v dashboard.Variable, path string) error {
|
||||
switch spec := v.Spec.(type) {
|
||||
case *dashboard.ListVariableSpec:
|
||||
pluginPath := path + ".spec.plugin"
|
||||
kind := VariablePluginKind(spec.Plugin.Kind)
|
||||
factory, ok := variablePluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown variable plugin kind %q; allowed values: %s", pluginPath, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(&spec.Plugin, factory, pluginPath)
|
||||
case *dashboard.TextVariableSpec:
|
||||
// TextVariables have no plugin, nothing to validate.
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: unsupported variable kind %q", path, v.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePanelPlugin(plugin *common.Plugin, path string) error {
|
||||
kind := PanelPluginKind(plugin.Kind)
|
||||
factory, ok := panelPluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown panel plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func validateQueryPlugin(plugin *common.Plugin, path string) error {
|
||||
kind := QueryPluginKind(plugin.Kind)
|
||||
factory, ok := queryPluginSpecs[kind]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: unknown query plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
|
||||
}
|
||||
return validateAndNormalizePluginSpec(plugin, factory, path)
|
||||
}
|
||||
|
||||
func formatEnum(values []any) string {
|
||||
parts := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
parts[i] = fmt.Sprintf("`%v`", v)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// validateAndNormalizePluginSpec validates the plugin spec and writes the typed
|
||||
// struct (with defaults) back into plugin.Spec so that DB storage and API
|
||||
// responses contain normalized values.
|
||||
func validateAndNormalizePluginSpec(plugin *common.Plugin, factory func() any, path string) error {
|
||||
if plugin.Kind == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin kind is required", path)
|
||||
}
|
||||
if plugin.Spec == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin spec is required", path)
|
||||
}
|
||||
// Re-marshal the spec and unmarshal into the typed struct.
|
||||
specJSON, err := json.Marshal(plugin.Spec)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
target := factory()
|
||||
decoder := json.NewDecoder(bytes.NewReader(specJSON))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(target); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
if err := validator.New().Struct(target); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
// Write the typed struct back so defaults are included.
|
||||
plugin.Spec = target
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryAllowedForPanel checks that the query plugin kind is permitted
|
||||
// for the given panel. For composite queries it recurses into sub-queries.
|
||||
func validateQueryAllowedForPanel(plugin common.Plugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
|
||||
queryKind := QueryPluginKind(plugin.Kind)
|
||||
if !slices.Contains(allowed, queryKind) {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s: query kind %q is not supported by panel kind %q", path, queryKind, panelKind)
|
||||
}
|
||||
|
||||
// For composite queries, validate each sub-query type.
|
||||
if queryKind == QueryKindComposite && plugin.Spec != nil {
|
||||
specJSON, err := json.Marshal(plugin.Spec)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
var composite struct {
|
||||
Queries []struct {
|
||||
Type qb.QueryType `json:"type"`
|
||||
} `json:"queries"`
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, &composite); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
for si, sub := range composite.Queries {
|
||||
pluginKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(allowed, pluginKind) {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
|
||||
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
|
||||
path, si, sub.Type, panelKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dashboardtypesv2
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -14,26 +14,26 @@ import (
|
||||
func TestValidateBigExample(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/perses.json")
|
||||
require.NoError(t, err, "reading example file")
|
||||
_, err = UnmarshalAndValidateJSON(data)
|
||||
_, err = UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "expected valid dashboard")
|
||||
}
|
||||
|
||||
func TestValidateDashboardWithSections(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/perses_with_sections.json")
|
||||
require.NoError(t, err, "reading example file")
|
||||
_, err = UnmarshalAndValidateJSON(data)
|
||||
_, err = UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "expected valid dashboard")
|
||||
}
|
||||
|
||||
func TestInvalidateNotAJSON(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte("not json"))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte("not json"))
|
||||
require.Error(t, err, "expected error for invalid JSON")
|
||||
}
|
||||
|
||||
func TestValidateEmptySpec(t *testing.T) {
|
||||
// no variables no panels
|
||||
data := []byte(`{}`)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "expected valid")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestValidateOnlyVariables(t *testing.T) {
|
||||
],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "expected valid")
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -169,7 +169,7 @@ func TestInvalidateOneInvalidPanel(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := UnmarshalAndValidateJSON(data)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.Error(t, err, "expected error for invalid panel plugin kind")
|
||||
require.Contains(t, err.Error(), "FakePanel", "error should mention FakePanel")
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error for unknown field")
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -323,7 +323,7 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected validation error")
|
||||
if tt.wantContain != "" {
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
@@ -531,7 +531,7 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -626,7 +626,7 @@ func TestValidateRequiredFields(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON([]byte(tt.data))
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
|
||||
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
|
||||
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
|
||||
})
|
||||
@@ -648,7 +648,7 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := UnmarshalAndValidateJSON(data)
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
// After validation+normalization, the plugin spec should be a typed struct.
|
||||
@@ -695,7 +695,7 @@ func TestNumberPanelDefaults(t *testing.T) {
|
||||
},
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := UnmarshalAndValidateJSON(data)
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
require.IsType(t, &NumberPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
|
||||
@@ -745,7 +745,7 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
}`)
|
||||
|
||||
// Step 1: Unmarshal + validate + normalize (what the API handler does).
|
||||
d, err := UnmarshalAndValidateJSON(input)
|
||||
d, err := UnmarshalAndValidateDashboardV2JSON(input)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
// Step 1.5: Verify struct fields have correct defaults (extra validation before storing).
|
||||
@@ -765,7 +765,7 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
require.NoError(t, err, "marshal for storage failed")
|
||||
|
||||
// Step 3: Unmarshal from JSON (simulates reading from DB).
|
||||
loaded, err := UnmarshalAndValidateJSON(stored)
|
||||
loaded, err := UnmarshalAndValidateDashboardV2JSON(stored)
|
||||
require.NoError(t, err, "unmarshal from storage failed")
|
||||
|
||||
// Step 3.5: Verify struct fields have correct defaults after loading (before returning in API).
|
||||
@@ -878,7 +878,7 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := UnmarshalAndValidateJSON(tc.data)
|
||||
_, err := UnmarshalAndValidateDashboardV2JSON(tc.data)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
@@ -1,274 +0,0 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/common"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
"github.com/perses/perses/pkg/model/api/v1/variable"
|
||||
)
|
||||
|
||||
// DashboardData is the SigNoz dashboard v2 spec shape. It mirrors
|
||||
// v1.DashboardSpec (Perses) field-for-field, except every common.Plugin
|
||||
// occurrence is replaced with a typed SigNoz plugin whose OpenAPI schema is a
|
||||
// per-site discriminated oneOf.
|
||||
//
|
||||
// Drift against Perses is guarded by TestDashboardDataMatchesPerses.
|
||||
// Leaf types (common.Display, v1.Link, dashboard.Layout, variable.*) are reused
|
||||
// directly — changes in those flow through automatically, and breaking changes
|
||||
// surface as compile errors in code that uses them.
|
||||
type DashboardData struct {
|
||||
Display *common.Display `json:"display,omitempty"`
|
||||
Datasources map[string]*DatasourceSpec `json:"datasources,omitempty"`
|
||||
Variables []Variable `json:"variables,omitempty"`
|
||||
Panels map[string]*Panel `json:"panels"`
|
||||
Layouts []Layout `json:"layouts"`
|
||||
Duration common.DurationString `json:"duration"`
|
||||
RefreshInterval common.DurationString `json:"refreshInterval,omitempty"`
|
||||
Links []v1.Link `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type Panel struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec PanelSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type PanelSpec struct {
|
||||
Display *v1.PanelDisplay `json:"display,omitempty"`
|
||||
Plugin PanelPlugin `json:"plugin"`
|
||||
Queries []Query `json:"queries,omitempty"`
|
||||
Links []v1.Link `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Query
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type Query struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec QuerySpec `json:"spec"`
|
||||
}
|
||||
|
||||
type QuerySpec struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Plugin QueryPlugin `json:"plugin"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Variable is the list/text sum type. Spec is set to *ListVariableSpec or
|
||||
// *TextVariableSpec by UnmarshalJSON based on Kind. The schema is a
|
||||
// discriminated oneOf (see JSONSchemaOneOf).
|
||||
type Variable struct {
|
||||
Kind variable.Kind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// ListVariableSpec mirrors dashboard.ListVariableSpec (variable.ListSpec
|
||||
// fields + Name) but with a typed VariablePlugin replacing common.Plugin.
|
||||
type ListVariableSpec struct {
|
||||
Display *variable.Display `json:"display,omitempty"`
|
||||
DefaultValue *variable.DefaultValue `json:"defaultValue,omitempty"`
|
||||
AllowAllValue bool `json:"allowAllValue"`
|
||||
AllowMultiple bool `json:"allowMultiple"`
|
||||
CustomAllValue string `json:"customAllValue,omitempty"`
|
||||
CapturingRegexp string `json:"capturingRegexp,omitempty"`
|
||||
Sort *variable.Sort `json:"sort,omitempty"`
|
||||
Plugin VariablePlugin `json:"plugin"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TextVariableSpec mirrors dashboard.TextVariableSpec (variable.TextSpec +
|
||||
// Name). No plugin.
|
||||
type TextVariableSpec struct {
|
||||
Display *variable.Display `json:"display,omitempty"`
|
||||
Value string `json:"value"`
|
||||
Constant bool `json:"constant,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Layout
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Layout is the dashboard layout sum type. Spec is populated by UnmarshalJSON
|
||||
// with the concrete layout spec struct (today only dashboard.GridLayoutSpec)
|
||||
// based on Kind. No plugin is involved, so we reuse the Perses spec types as
|
||||
// leaf imports.
|
||||
type Layout struct {
|
||||
Kind dashboard.LayoutKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Datasource
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type DatasourceSpec struct {
|
||||
Display *common.Display `json:"display,omitempty"`
|
||||
Default bool `json:"default"`
|
||||
Plugin DatasourcePlugin `json:"plugin"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Unmarshal + validate entry point
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// UnmarshalAndValidateJSON unmarshals the JSON into a
|
||||
// DashboardData and validates cross-field rules. Plugin kind and
|
||||
// plugin-spec shape are already enforced by the typed plugin UnmarshalJSON
|
||||
// paths, so only rules that can't be expressed in the type system run here.
|
||||
func UnmarshalAndValidateJSON(data []byte) (*DashboardData, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
var d DashboardData
|
||||
if err := dec.Decode(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDashboard(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Plugin kind → spec factory maps
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Each plugin's UnmarshalJSON uses its factory map to instantiate a typed
|
||||
// Spec based on the Kind field. Single source of truth for "which kinds
|
||||
// exist" — JSONSchemaOneOf iterates the same maps.
|
||||
var (
|
||||
panelPluginSpecs = map[PanelPluginKind]func() any{
|
||||
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
|
||||
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
|
||||
PanelKindNumber: func() any { return new(NumberPanelSpec) },
|
||||
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
|
||||
PanelKindList: func() any { return new(ListPanelSpec) },
|
||||
}
|
||||
queryPluginSpecs = map[QueryPluginKind]func() any{
|
||||
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
|
||||
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
|
||||
QueryKindFormula: func() any { return new(FormulaSpec) },
|
||||
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
|
||||
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
|
||||
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
|
||||
}
|
||||
variablePluginSpecs = map[VariablePluginKind]func() any{
|
||||
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
|
||||
VariableKindQuery: func() any { return new(QueryVariableSpec) },
|
||||
VariableKindCustom: func() any { return new(CustomVariableSpec) },
|
||||
VariableKindTextbox: func() any { return new(TextboxVariableSpec) },
|
||||
}
|
||||
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
|
||||
DatasourceKindSigNoz: func() any { return new(struct{}) },
|
||||
}
|
||||
|
||||
// layoutSpecs is the layout sum type factory. Perses only defines
|
||||
// KindGridLayout today; adding a new kind upstream surfaces as an
|
||||
// "unknown layout kind" runtime error here until we add it.
|
||||
layoutSpecs = map[dashboard.LayoutKind]func() any{
|
||||
dashboard.KindGridLayout: func() any { return new(dashboard.GridLayoutSpec) },
|
||||
}
|
||||
|
||||
// allowedQueryKinds maps each panel plugin kind to the query plugin
|
||||
// kinds it supports. Composite sub-query types are mapped to these
|
||||
// same kind strings via compositeSubQueryTypeToPluginKind.
|
||||
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
|
||||
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindList: {QueryKindBuilder},
|
||||
}
|
||||
|
||||
// compositeSubQueryTypeToPluginKind maps CompositeQuery sub-query type
|
||||
// strings to the equivalent top-level query plugin kind for validation.
|
||||
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
|
||||
qb.QueryTypeBuilder: QueryKindBuilder,
|
||||
qb.QueryTypeFormula: QueryKindFormula,
|
||||
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
|
||||
qb.QueryTypePromQL: QueryKindPromQL,
|
||||
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
|
||||
}
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Cross-field validation
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func validateDashboard(d DashboardData) error {
|
||||
for key, panel := range d.Panels {
|
||||
if panel == nil {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
|
||||
}
|
||||
path := fmt.Sprintf("spec.panels.%s", key)
|
||||
panelKind := panel.Spec.Plugin.Kind
|
||||
allowed := allowedQueryKinds[panelKind]
|
||||
for qi, q := range panel.Spec.Queries {
|
||||
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
|
||||
if err := validateQueryAllowedForPanel(q.Spec.Plugin, allowed, panelKind, queryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryAllowedForPanel checks that the query plugin kind is permitted
|
||||
// for the given panel. For composite queries it recurses into sub-queries.
|
||||
func validateQueryAllowedForPanel(plugin QueryPlugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
|
||||
if !slices.Contains(allowed, plugin.Kind) {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput,
|
||||
"%s: query kind %q is not supported by panel kind %q", path, plugin.Kind, panelKind)
|
||||
}
|
||||
|
||||
if plugin.Kind != QueryKindComposite {
|
||||
return nil
|
||||
}
|
||||
composite, ok := plugin.Spec.(*CompositeQuerySpec)
|
||||
if !ok || composite == nil {
|
||||
return nil
|
||||
}
|
||||
specJSON, err := json.Marshal(composite)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
var subs struct {
|
||||
Queries []struct {
|
||||
Type qb.QueryType `json:"type"`
|
||||
} `json:"queries"`
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, &subs); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "%s.spec", path)
|
||||
}
|
||||
for si, sub := range subs.Queries {
|
||||
subKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(allowed, subKind) {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput,
|
||||
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
|
||||
path, si, sub.Type, panelKind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
// TestDashboardDataMatchesPerses asserts that DashboardData
|
||||
// and every nested SigNoz-owned type cover the JSON field set of their Perses
|
||||
// counterpart. It fails loud if Perses adds, renames, or removes a field
|
||||
// upstream — turning silent drift into a CI signal on the next Perses bump.
|
||||
//
|
||||
// The test does NOT check field *types* (plugin fields intentionally diverge:
|
||||
// our typed plugins vs Perses's common.Plugin). It only checks that every
|
||||
// json-tagged field in the Perses struct exists in ours under the same tag.
|
||||
//
|
||||
// Wrapper types we re-derive (variable.ListSpec is flattened into our
|
||||
// ListVariableSpec, same for TextSpec) are compared against the flattened
|
||||
// field set.
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/perses/perses/pkg/model/api/v1"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDashboardDataMatchesPerses(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ours reflect.Type
|
||||
perses reflect.Type
|
||||
}{
|
||||
{"DashboardSpec", typeOf[DashboardData](), typeOf[v1.DashboardSpec]()},
|
||||
{"PanelSpec", typeOf[PanelSpec](), typeOf[v1.PanelSpec]()},
|
||||
{"QuerySpec", typeOf[QuerySpec](), typeOf[v1.QuerySpec]()},
|
||||
{"DatasourceSpec", typeOf[DatasourceSpec](), typeOf[v1.DatasourceSpec]()},
|
||||
// ListVariableSpec/TextVariableSpec embed variable.ListSpec/TextSpec
|
||||
// plus a Name field. We flatten the Perses shape to compare.
|
||||
{"ListVariableSpec", typeOf[ListVariableSpec](), typeOf[dashboard.ListVariableSpec]()},
|
||||
{"TextVariableSpec", typeOf[TextVariableSpec](), typeOf[dashboard.TextVariableSpec]()},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
ours := jsonFields(c.ours)
|
||||
perses := jsonFields(c.perses)
|
||||
|
||||
missing := sortedDiff(perses, ours)
|
||||
extra := sortedDiff(ours, perses)
|
||||
|
||||
assert.Empty(t, missing,
|
||||
"DashboardData (%s) is missing json fields present on Perses %s — upstream likely added or renamed a field",
|
||||
c.ours.Name(), c.perses.Name())
|
||||
assert.Empty(t, extra,
|
||||
"DashboardData (%s) has json fields absent on Perses %s — upstream likely removed a field or we added one without the counterpart",
|
||||
c.ours.Name(), c.perses.Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// jsonFields returns the set of json tag names for a struct, flattening
|
||||
// anonymous embedded fields (matching encoding/json behavior).
|
||||
func jsonFields(t reflect.Type) map[string]struct{} {
|
||||
out := map[string]struct{}{}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return out
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
// Skip unexported fields (e.g., dashboard.TextVariableSpec has an
|
||||
// unexported `variableSpec` interface tag).
|
||||
if !f.IsExported() && !f.Anonymous {
|
||||
continue
|
||||
}
|
||||
tag := f.Tag.Get("json")
|
||||
name := strings.Split(tag, ",")[0]
|
||||
// Anonymous embed with empty json name (no tag, or `json:",inline"` /
|
||||
// `json:",omitempty"`-style options-only tag) is flattened by encoding/json.
|
||||
if f.Anonymous && name == "" {
|
||||
for k := range jsonFields(f.Type) {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if tag == "-" || name == "" {
|
||||
continue
|
||||
}
|
||||
out[name] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sortedDiff returns keys in a but not in b, sorted.
|
||||
func sortedDiff(a, b map[string]struct{}) []string {
|
||||
var diff []string
|
||||
for k := range a {
|
||||
if _, ok := b[k]; !ok {
|
||||
diff = append(diff, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(diff)
|
||||
return diff
|
||||
}
|
||||
|
||||
func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() }
|
||||
@@ -1,388 +0,0 @@
|
||||
package dashboardtypesv2
|
||||
|
||||
// Typed plugin envelopes for the four plugin sites in DashboardData.
|
||||
// Each plugin:
|
||||
// - Has Kind (typed enum) and Spec (any, resolved at runtime via UnmarshalJSON)
|
||||
// - Implements JSONSchemaOneOf so the reflector emits a per-site discriminated
|
||||
// oneOf over only the kinds valid at that site
|
||||
// - Implements UnmarshalJSON that dispatches Spec to the concrete type based
|
||||
// on Kind, using the factory maps in dashboard_v2.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/perses/perses/pkg/model/api/v1/dashboard"
|
||||
"github.com/perses/perses/pkg/model/api/v1/variable"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type PanelPlugin struct {
|
||||
Kind PanelPluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
// PrepareJSONSchema drops the reflected struct shape (type: object, properties)
|
||||
// from the envelope so that only the JSONSchemaOneOf result binds. Mirrors the
|
||||
// pattern in swaggest/jsonschema-go's built-in oneOf helper.
|
||||
func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (PanelPlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
panelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)},
|
||||
panelPluginVariant[BarChartPanelSpec]{Kind: string(PanelKindBarChart)},
|
||||
panelPluginVariant[NumberPanelSpec]{Kind: string(PanelKindNumber)},
|
||||
panelPluginVariant[PieChartPanelSpec]{Kind: string(PanelKindPieChart)},
|
||||
panelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
|
||||
panelPluginVariant[HistogramPanelSpec]{Kind: string(PanelKindHistogram)},
|
||||
panelPluginVariant[ListPanelSpec]{Kind: string(PanelKindList)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := panelPluginSpecs[PanelPluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown panel plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = PanelPluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type panelPluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v panelPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Query plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type QueryPlugin struct {
|
||||
Kind QueryPluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (QueryPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (QueryPlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
queryPluginVariant[BuilderQuerySpec]{Kind: string(QueryKindBuilder)},
|
||||
queryPluginVariant[CompositeQuerySpec]{Kind: string(QueryKindComposite)},
|
||||
queryPluginVariant[FormulaSpec]{Kind: string(QueryKindFormula)},
|
||||
queryPluginVariant[PromQLQuerySpec]{Kind: string(QueryKindPromQL)},
|
||||
queryPluginVariant[ClickHouseSQLQuerySpec]{Kind: string(QueryKindClickHouseSQL)},
|
||||
queryPluginVariant[TraceOperatorSpec]{Kind: string(QueryKindTraceOperator)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *QueryPlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := queryPluginSpecs[QueryPluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown query plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = QueryPluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type queryPluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v queryPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type VariablePlugin struct {
|
||||
Kind VariablePluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (VariablePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (VariablePlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
variablePluginVariant[DynamicVariableSpec]{Kind: string(VariableKindDynamic)},
|
||||
variablePluginVariant[QueryVariableSpec]{Kind: string(VariableKindQuery)},
|
||||
variablePluginVariant[CustomVariableSpec]{Kind: string(VariableKindCustom)},
|
||||
variablePluginVariant[TextboxVariableSpec]{Kind: string(VariableKindTextbox)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VariablePlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := variablePluginSpecs[VariablePluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown variable plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = VariablePluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type variablePluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v variablePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Datasource plugin
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
type DatasourcePlugin struct {
|
||||
Kind DatasourcePluginKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (DatasourcePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (DatasourcePlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
datasourcePluginVariant[struct{}]{Kind: string(DatasourceKindSigNoz)},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DatasourcePlugin) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := datasourcePluginSpecs[DatasourcePluginKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown datasource plugin kind %q", kind)
|
||||
}
|
||||
spec, err := decodePluginSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Kind = DatasourcePluginKind(kind)
|
||||
p.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type datasourcePluginVariant[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v datasourcePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variable envelope (list/text sum type)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (Variable) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (Variable) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
variableEnvelope[ListVariableSpec]{Kind: string(variable.KindList)},
|
||||
variableEnvelope[TextVariableSpec]{Kind: string(variable.KindText)},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch kind {
|
||||
case string(variable.KindList):
|
||||
spec, err := decodeVariableSpec(specJSON, new(ListVariableSpec))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Kind = variable.KindList
|
||||
v.Spec = spec
|
||||
case string(variable.KindText):
|
||||
spec, err := decodeVariableSpec(specJSON, new(TextVariableSpec))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Kind = variable.KindText
|
||||
v.Spec = spec
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown variable kind %q", kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type variableEnvelope[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v variableEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Layout envelope (grid, potentially more in the future)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (Layout) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
func (Layout) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
layoutEnvelope[dashboard.GridLayoutSpec]{Kind: string(dashboard.KindGridLayout)},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Layout) UnmarshalJSON(data []byte) error {
|
||||
kind, specJSON, err := splitKindSpec(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory, ok := layoutSpecs[dashboard.LayoutKind(kind)]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "unknown layout kind %q", kind)
|
||||
}
|
||||
spec, err := decodeVariableSpec(specJSON, factory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Kind = dashboard.LayoutKind(kind)
|
||||
l.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
type layoutEnvelope[S any] struct {
|
||||
Kind string `json:"kind" required:"true"`
|
||||
Spec S `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
func (v layoutEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return applyKindEnum(s, v.Kind)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Helpers
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// splitKindSpec parses a {"kind": "...", "spec": {...}} envelope and returns
|
||||
// kind and the raw spec bytes for typed decoding.
|
||||
func splitKindSpec(data []byte) (string, []byte, error) {
|
||||
var head struct {
|
||||
Kind string `json:"kind"`
|
||||
Spec json.RawMessage `json:"spec"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &head); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return head.Kind, head.Spec, nil
|
||||
}
|
||||
|
||||
// decodePluginSpec strict-decodes a plugin spec JSON into target and runs
|
||||
// struct-tag validation (go-playground/validator). Strictness matches the
|
||||
// previous validateAndNormalizePluginSpec contract: unknown fields rejected,
|
||||
// required-field violations surfaced. Returns target on success.
|
||||
func decodePluginSpec(specJSON []byte, target any) (any, error) {
|
||||
if len(specJSON) == 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec is required")
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(specJSON))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(target); err != nil {
|
||||
return nil, fmt.Errorf("decode spec: %w", err)
|
||||
}
|
||||
if err := validator.New().Struct(target); err != nil {
|
||||
return nil, fmt.Errorf("validate spec: %w", err)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// decodeVariableSpec is lenient: used for ListVariableSpec / TextVariableSpec
|
||||
// where Perses historically ignored unknown fields (e.g., stray `plugin` on
|
||||
// text variables in existing fixtures).
|
||||
func decodeVariableSpec(specJSON []byte, target any) (any, error) {
|
||||
if len(specJSON) == 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "spec is required")
|
||||
}
|
||||
if err := json.Unmarshal(specJSON, target); err != nil {
|
||||
return nil, fmt.Errorf("decode spec: %w", err)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// clearOneOfParentShape drops Type and Properties on a schema that also has a
|
||||
// JSONSchemaOneOf. Swaggest emits both the reflected struct shape and the
|
||||
// oneOf side by side; OAS-wise the oneOf is the binding constraint so the
|
||||
// properties/type are redundant noise. This mirrors swaggest's own built-in
|
||||
// oneOf helper's PrepareJSONSchema.
|
||||
func clearOneOfParentShape(s *jsonschema.Schema) error {
|
||||
s.Type = nil
|
||||
s.Properties = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyKindEnum narrows the `kind` property of a oneOf variant schema to a
|
||||
// single permitted string, producing `kind: { type: string, enum: [kind] }`.
|
||||
// Each variant calls this from PrepareJSONSchema because Go generics can't
|
||||
// propagate struct tag values (so we can't write enum:"..." on Kind).
|
||||
func applyKindEnum(schema *jsonschema.Schema, kind string) error {
|
||||
kindProp, ok := schema.Properties["kind"]
|
||||
if !ok || kindProp.TypeObject == nil {
|
||||
return fmt.Errorf("variant schema missing `kind` property")
|
||||
}
|
||||
kindProp.TypeObject.WithEnum(kind)
|
||||
schema.Properties["kind"] = kindProp
|
||||
return nil
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package dashboardtypesv2
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -83,28 +81,12 @@ type BuilderQuerySpec struct {
|
||||
func (b *BuilderQuerySpec) UnmarshalJSON(data []byte) error {
|
||||
spec, err := qb.UnmarshalBuilderQueryBySignal(data)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid builder query spec")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid builder query spec")
|
||||
}
|
||||
b.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareJSONSchema drops the reflected struct shape so only the
|
||||
// JSONSchemaOneOf result binds.
|
||||
func (BuilderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return clearOneOfParentShape(s)
|
||||
}
|
||||
|
||||
// JSONSchemaOneOf exposes the three signal-dispatched shapes a builder query
|
||||
// can take. Mirrors qb.UnmarshalBuilderQueryBySignal's runtime dispatch.
|
||||
func (BuilderQuerySpec) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
qb.QueryBuilderQuery[qb.LogAggregation]{},
|
||||
qb.QueryBuilderQuery[qb.MetricAggregation]{},
|
||||
qb.QueryBuilderQuery[qb.TraceAggregation]{},
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// SigNoz panel plugin specs
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -290,7 +272,7 @@ func (t TimePreference) MarshalJSON() ([]byte, error) {
|
||||
func (t *TimePreference) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid timePreference: must be a string, one of `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid timePreference: must be a string, one of `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`")
|
||||
}
|
||||
if v == "" {
|
||||
*t = TimePreferenceGlobalTime
|
||||
@@ -302,7 +284,7 @@ func (t *TimePreference) UnmarshalJSON(data []byte) error {
|
||||
*t = tp
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid timePreference %q: must be `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid timePreference %q: must be `global_time`, `last_5_min`, `last_15_min`, `last_30_min`, `last_1_hr`, `last_6_hr`, `last_1_day`, `last_3_days`, `last_1_week`, or `last_1_month`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +313,7 @@ func (l LegendPosition) MarshalJSON() ([]byte, error) {
|
||||
func (l *LegendPosition) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid legend position: must be a string, one of `bottom` or `right`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid legend position: must be a string, one of `bottom` or `right`")
|
||||
}
|
||||
if v == "" {
|
||||
*l = LegendPositionBottom
|
||||
@@ -343,7 +325,7 @@ func (l *LegendPosition) UnmarshalJSON(data []byte) error {
|
||||
*l = lp
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid legend position %q: must be `bottom` or `right`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid legend position %q: must be `bottom` or `right`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +354,7 @@ func (f ThresholdFormat) MarshalJSON() ([]byte, error) {
|
||||
func (f *ThresholdFormat) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid threshold format: must be a string, one of `text` or `background`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid threshold format: must be a string, one of `text` or `background`")
|
||||
}
|
||||
if v == "" {
|
||||
*f = ThresholdFormatText
|
||||
@@ -384,7 +366,7 @@ func (f *ThresholdFormat) UnmarshalJSON(data []byte) error {
|
||||
*f = tf
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid threshold format %q: must be `text` or `background`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid threshold format %q: must be `text` or `background`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +406,7 @@ func (o ComparisonOperator) MarshalJSON() ([]byte, error) {
|
||||
func (o *ComparisonOperator) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid comparison operator: must be a string, one of `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid comparison operator: must be a string, one of `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`")
|
||||
}
|
||||
if v == "" {
|
||||
*o = ComparisonOperatorGT
|
||||
@@ -438,7 +420,7 @@ func (o *ComparisonOperator) UnmarshalJSON(data []byte) error {
|
||||
*o = co
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid comparison operator %q: must be `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid comparison operator %q: must be `>`, `<`, `>=`, `<=`, `=`, `above`, `below`, `above_or_equal`, `below_or_equal`, `equal`, or `not_equal`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +451,7 @@ func (li LineInterpolation) MarshalJSON() ([]byte, error) {
|
||||
func (li *LineInterpolation) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line interpolation: must be a string, one of `linear`, `spline`, `step_after`, or `step_before`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid line interpolation: must be a string, one of `linear`, `spline`, `step_after`, or `step_before`")
|
||||
}
|
||||
if v == "" {
|
||||
*li = LineInterpolationSpline
|
||||
@@ -481,7 +463,7 @@ func (li *LineInterpolation) UnmarshalJSON(data []byte) error {
|
||||
*li = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line interpolation %q: must be `linear`, `spline`, `step_after`, or `step_before`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid line interpolation %q: must be `linear`, `spline`, `step_after`, or `step_before`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +492,7 @@ func (ls LineStyle) MarshalJSON() ([]byte, error) {
|
||||
func (ls *LineStyle) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line style: must be a string, one of `solid` or `dashed`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid line style: must be a string, one of `solid` or `dashed`")
|
||||
}
|
||||
if v == "" {
|
||||
*ls = LineStyleSolid
|
||||
@@ -522,7 +504,7 @@ func (ls *LineStyle) UnmarshalJSON(data []byte) error {
|
||||
*ls = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid line style %q: must be `solid` or `dashed`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid line style %q: must be `solid` or `dashed`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,7 +534,7 @@ func (fm FillMode) MarshalJSON() ([]byte, error) {
|
||||
func (fm *FillMode) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
|
||||
}
|
||||
if v == "" {
|
||||
*fm = FillModeSolid
|
||||
@@ -564,7 +546,7 @@ func (fm *FillMode) UnmarshalJSON(data []byte) error {
|
||||
*fm = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,12 +593,12 @@ func (p *PrecisionOption) UnmarshalJSON(data []byte) error {
|
||||
p.String = valuer.NewString(strconv.Itoa(n))
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option %d: must be `0`, `1`, `2`, `3`, `4`, or `full`", n)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid precision option %d: must be `0`, `1`, `2`, `3`, `4`, or `full`", n)
|
||||
}
|
||||
}
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option: must be `0`, `1`, `2`, `3`, `4`, or `full`")
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid precision option: must be `0`, `1`, `2`, `3`, `4`, or `full`")
|
||||
}
|
||||
if v == "" {
|
||||
*p = PrecisionOption2
|
||||
@@ -628,6 +610,6 @@ func (p *PrecisionOption) UnmarshalJSON(data []byte) error {
|
||||
*p = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardInvalidInput, "invalid precision option %q: must be `0`, `1`, `2`, `3`, `4`, or `full`", v)
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid precision option %q: must be `0`, `1`, `2`, `3`, `4`, or `full`", v)
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,7 @@ type Store interface {
|
||||
GetActiveByOrgIDAndName(context.Context, valuer.UUID, string) (*ServiceAccount, error)
|
||||
GetByID(context.Context, valuer.UUID) (*ServiceAccount, error)
|
||||
GetByIDAndStatus(context.Context, valuer.UUID, ServiceAccountStatus) (*ServiceAccount, error)
|
||||
GetServiceAccountsByOrgIDAndRoleID(context.Context, valuer.UUID, valuer.UUID) ([]*ServiceAccount, error)
|
||||
CountByOrgID(context.Context, valuer.UUID) (int64, error)
|
||||
List(context.Context, valuer.UUID) ([]*ServiceAccount, error)
|
||||
Update(context.Context, valuer.UUID, *ServiceAccount) error
|
||||
|
||||
Reference in New Issue
Block a user