blob: 635c1dabcae622b6cc5a0e97e6213c583de67075 [file]
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// Package validate provides functions to validate the provided objects.
package validate
import (
"errors"
"fmt"
"path/filepath"
"strings"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
"github.com/apache/skywalking-banyandb/pkg/pipeline/sdk"
)
const reservedTagSeparator = "#"
func validateTagName(name string) error {
if strings.Contains(name, reservedTagSeparator) {
return fmt.Errorf("tag name %q must not contain reserved character %q", name, reservedTagSeparator)
}
return nil
}
// Group validates the provided Group object.
func Group(group *commonv1.Group) error {
if group.Metadata == nil {
return errors.New("metadata is required")
}
if group.Metadata.Name == "" {
return errors.New("metadata.name is required")
}
if group.Catalog == commonv1.Catalog_CATALOG_UNSPECIFIED {
return errors.New("catalog is unspecified")
}
if group.Catalog == commonv1.Catalog_CATALOG_PROPERTY {
if group.ResourceOpts == nil {
return errors.New("resourceOpts is nil")
}
if group.ResourceOpts.ShardNum <= 0 {
return errors.New("shardNum is invalid")
}
if group.ResourceOpts.SegmentInterval != nil {
return errors.New("segmentInterval should be nil")
}
if group.ResourceOpts.Ttl != nil {
return errors.New("ttl should be nil")
}
return nil
}
return GroupForNonProperty(group)
}
// GroupForNonProperty validates the provided Group object for Stream or Measure.
// It checks for nil values, empty strings, and unspecified enum values.
func GroupForNonProperty(group *commonv1.Group) error {
if group == nil {
return errors.New("group is nil")
}
if group.Metadata == nil {
return errors.New("group metadata is nil")
}
if group.Metadata.Name == "" {
return errors.New("group name is empty")
}
if group.Catalog == commonv1.Catalog_CATALOG_UNSPECIFIED {
return errors.New("group catalog is unspecified")
}
if group.ResourceOpts == nil {
return errors.New("group resourceOpts is nil")
}
if group.ResourceOpts.ShardNum <= 0 {
return errors.New("group shardNum is invalid")
}
if group.ResourceOpts.SegmentInterval == nil {
return errors.New("group segmentInterval is nil")
}
if group.ResourceOpts.SegmentInterval.Num <= 0 {
return errors.New("group segmentInterval num is invalid")
}
if group.ResourceOpts.SegmentInterval.Unit == commonv1.IntervalRule_UNIT_UNSPECIFIED {
return errors.New("group segmentInterval unit is unspecified")
}
if group.ResourceOpts.Ttl == nil {
return errors.New("group ttl is nil")
}
if group.ResourceOpts.Ttl.Num <= 0 {
return errors.New("group ttl num is invalid")
}
if group.ResourceOpts.Ttl.Unit == commonv1.IntervalRule_UNIT_UNSPECIFIED {
return errors.New("group ttl unit is unspecified")
}
if pipelineCfg := group.GetPipeline(); pipelineCfg != nil {
if validateErr := validateTracePipelineConfig(pipelineCfg); validateErr != nil {
return fmt.Errorf("group pipeline config is invalid: %w", validateErr)
}
}
return nil
}
// validateTracePipelineConfig validates the embedded TracePipelineConfig on a Group.
// It checks each plugin's sampler for trusted-dir-safe path shape and ABI version,
// then delegates bounds validation to the generated proto validator.
func validateTracePipelineConfig(cfg *commonv1.TracePipelineConfig) error {
if validateErr := cfg.Validate(); validateErr != nil {
return fmt.Errorf("proto validation failed: %w", validateErr)
}
for idx, plugin := range cfg.GetPlugins() {
sp := plugin.GetSampler()
if sp == nil {
continue
}
if sp.GetPath() == "" {
return fmt.Errorf("plugins[%d].sampler.path is empty", idx)
}
// Reject absolute paths and path segments that escape via "..".
cleaned := filepath.Clean(sp.GetPath())
if filepath.IsAbs(cleaned) {
return fmt.Errorf("plugins[%d].sampler.path must be a relative path, got %q", idx, sp.GetPath())
}
if cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
return fmt.Errorf("plugins[%d].sampler.path %q escapes the trusted directory", idx, sp.GetPath())
}
if sp.GetAbiVersion() != sdk.ABIVersion {
return fmt.Errorf("plugins[%d].sampler.abi_version must be %d, got %d", idx, sdk.ABIVersion, sp.GetAbiVersion())
}
}
for idx, stage := range cfg.GetStages() {
for jdx, plugin := range stage.GetPlugins() {
if plugin.GetSampler() != nil {
return fmt.Errorf("stages[%d].plugins[%d]: stages[].plugins are not supported in v1; declare plugins at the top level", idx, jdx)
}
}
}
return nil
}
// Stream validates the provided Stream object.
// It checks for nil values, empty strings, and unspecified enum values.
func Stream(stream *databasev1.Stream) error {
if stream == nil {
return errors.New("stream is nil")
}
if stream.Metadata == nil {
return errors.New("stream metadata is nil")
}
if stream.Metadata.Name == "" {
return errors.New("stream name is empty")
}
if stream.Metadata.Group == "" {
return errors.New("stream group is empty")
}
if len(stream.TagFamilies) == 0 {
return errors.New("stream tag families is empty")
}
if stream.Entity == nil {
return errors.New("stream entity is nil")
}
if len(stream.Entity.TagNames) == 0 {
return errors.New("stream entity tag names is empty")
}
return tagFamily(stream.TagFamilies)
}
// Measure validates the provided Measure object.
// It checks for nil values, empty strings, and unspecified enum values.
func Measure(measure *databasev1.Measure) error {
if measure == nil {
return errors.New("measure is nil")
}
if measure.Metadata == nil {
return errors.New("measure metadata is nil")
}
if measure.Metadata.Name == "" {
return errors.New("measure name is empty")
}
if measure.Metadata.Group == "" {
return errors.New("measure group is empty")
}
if measure.Entity == nil {
return errors.New("measure entity is nil")
}
if len(measure.Entity.TagNames) == 0 {
return errors.New("measure entity tag names is empty")
}
for i := range measure.Fields {
if measure.Fields[i].Name == "" {
return errors.New("field name is empty")
}
if measure.Fields[i].FieldType == databasev1.FieldType_FIELD_TYPE_UNSPECIFIED {
return errors.New("field type is unspecified")
}
if measure.Fields[i].CompressionMethod == databasev1.CompressionMethod_COMPRESSION_METHOD_UNSPECIFIED {
return errors.New("compression method is unspecified")
}
if measure.Fields[i].CompressionMethod == databasev1.CompressionMethod_COMPRESSION_METHOD_UNSPECIFIED {
return errors.New("compression method is unspecified")
}
}
if len(measure.TagFamilies) == 0 {
return errors.New("measure tag families is empty")
}
if measure.IndexMode && len(measure.Fields) > 0 {
return errors.New("index mode is enabled, but fields are not empty")
}
return tagFamily(measure.TagFamilies)
}
// CheckShardingKeySubset checks whether every ShardingKey tag exists in Entity tags
// and the shared tags appear in the same relative order.
func CheckShardingKeySubset(measure *databasev1.Measure) error {
if measure == nil || measure.Entity == nil || measure.ShardingKey == nil || len(measure.ShardingKey.TagNames) == 0 {
return nil
}
// A single entity tag may represent a composite identifier, e.g. OAP's entity_id,
// which can already encode the sharding key fields such as service_id.
// In that case, literal tag-name subset validation would produce false positives.
if len(measure.Entity.TagNames) == 1 {
return nil
}
entityIndex := make(map[string]int, len(measure.Entity.TagNames))
for idx, tag := range measure.Entity.TagNames {
entityIndex[tag] = idx
}
prevPos := -1
for _, shardTag := range measure.ShardingKey.TagNames {
pos, exists := entityIndex[shardTag]
if !exists {
return fmt.Errorf("ShardingKey tag %q is not present in Entity tags %v", shardTag, measure.Entity.TagNames)
}
if pos <= prevPos {
return fmt.Errorf("ShardingKey %v is not in the same relative order as Entity tags %v",
measure.ShardingKey.TagNames, measure.Entity.TagNames)
}
prevPos = pos
}
return nil
}
// Trace validates the provided Trace object.
// It checks for nil values, empty strings, and unspecified enum values.
func Trace(trace *databasev1.Trace) error {
if trace == nil {
return errors.New("trace is nil")
}
if trace.Metadata == nil {
return errors.New("trace metadata is nil")
}
if trace.Metadata.Name == "" {
return errors.New("trace name is empty")
}
if trace.Metadata.Group == "" {
return errors.New("trace group is empty")
}
if len(trace.Tags) == 0 {
return errors.New("trace tags is empty")
}
if trace.TraceIdTagName == "" {
return errors.New("trace_id_tag_name is empty")
}
if err := validateTagName(trace.TraceIdTagName); err != nil {
return err
}
if trace.SpanIdTagName == "" {
return errors.New("span_id_tag_name is empty")
}
if err := validateTagName(trace.SpanIdTagName); err != nil {
return err
}
if trace.TimestampTagName == "" {
return errors.New("timestamp_tag_name is empty")
}
if err := validateTagName(trace.TimestampTagName); err != nil {
return err
}
for i := range trace.Tags {
if trace.Tags[i].Name == "" {
return errors.New("trace tag name is empty")
}
if err := validateTagName(trace.Tags[i].Name); err != nil {
return err
}
if trace.Tags[i].Type == databasev1.TagType_TAG_TYPE_UNSPECIFIED {
return errors.New("trace tag type is unspecified")
}
}
return nil
}
// TraceUpdate validates the provided Trace update operation.
// It ensures that reserved tags cannot be deleted.
func TraceUpdate(prevTrace, newTrace *databasev1.Trace) error {
reservedTags := map[string]struct{}{
newTrace.GetTraceIdTagName(): {},
newTrace.GetTimestampTagName(): {},
newTrace.GetSpanIdTagName(): {},
}
newTagSet := make(map[string]struct{})
for _, tag := range newTrace.GetTags() {
newTagSet[tag.GetName()] = struct{}{}
}
for _, prevTag := range prevTrace.GetTags() {
if _, exists := newTagSet[prevTag.GetName()]; !exists {
if _, isReserved := reservedTags[prevTag.GetName()]; isReserved {
return errors.New("cannot delete reserved tag " + prevTag.GetName())
}
}
}
return nil
}
func tagFamily(tagFamilies []*databasev1.TagFamilySpec) error {
for i := range tagFamilies {
if tagFamilies[i].Name == "" {
return errors.New("tag family name is empty")
}
for j := range tagFamilies[i].Tags {
if tagFamilies[i].Tags[j].Name == "" {
return errors.New("tag name is empty")
}
if err := validateTagName(tagFamilies[i].Tags[j].Name); err != nil {
return err
}
if tagFamilies[i].Tags[j].Type == databasev1.TagType_TAG_TYPE_UNSPECIFIED {
return errors.New("tag type is unspecified")
}
}
}
return nil
}
// IndexRule validates the provided IndexRule object.
// It checks for nil values, empty strings, and unspecified enum values.
func IndexRule(indexRule *databasev1.IndexRule) error {
if indexRule == nil {
return errors.New("indexRule is nil")
}
if indexRule.Metadata == nil {
return errors.New("indexRule metadata is nil")
}
if indexRule.Metadata.Name == "" {
return errors.New("indexRule name is empty")
}
if indexRule.Metadata.Group == "" {
return errors.New("indexRule group is empty")
}
if indexRule.Metadata.Id <= 0 {
return errors.New("indexRule id is invalid")
}
if len(indexRule.Tags) == 0 {
return errors.New("indexRule tags is empty")
}
if indexRule.Type == databasev1.IndexRule_TYPE_UNSPECIFIED {
return errors.New("indexRule type is unspecified")
}
return nil
}
// IndexRuleBinding validates the provided IndexRuleBinding object.
// It checks for nil values, empty strings, and unspecified enum values.
func IndexRuleBinding(indexRuleBinding *databasev1.IndexRuleBinding) error {
if indexRuleBinding == nil {
return errors.New("indexRuleBinding is nil")
}
if indexRuleBinding.Metadata == nil {
return errors.New("indexRuleBinding metadata is nil")
}
if indexRuleBinding.Metadata.Name == "" {
return errors.New("indexRuleBinding name is empty")
}
if indexRuleBinding.Metadata.Group == "" {
return errors.New("indexRuleBinding group is empty")
}
if indexRuleBinding.Subject == nil {
return errors.New("indexRuleBinding subject is nil")
}
if indexRuleBinding.Subject.Name == "" {
return errors.New("indexRuleBinding subject name is empty")
}
if indexRuleBinding.Subject.Catalog == commonv1.Catalog_CATALOG_UNSPECIFIED {
return errors.New("indexRuleBinding subject catalog is unspecified")
}
if len(indexRuleBinding.Rules) == 0 {
return errors.New("indexRuleBinding rules is empty")
}
return nil
}
// TopNAggregation validates the provided TopNAggregation object.
// It checks for nil values, empty strings, and unspecified enum values.
func TopNAggregation(topNAggregation *databasev1.TopNAggregation) error {
if topNAggregation == nil {
return errors.New("topNAggregation is nil")
}
if topNAggregation.Metadata == nil {
return errors.New("topNAggregation metadata is nil")
}
if topNAggregation.Metadata.Name == "" {
return errors.New("topNAggregation name is empty")
}
if topNAggregation.Metadata.Group == "" {
return errors.New("topNAggregation group is empty")
}
if topNAggregation.SourceMeasure == nil {
return errors.New("topNAggregation sourceMeasure is nil")
}
if topNAggregation.SourceMeasure.Name == "" {
return errors.New("topNAggregation sourceMeasure name is empty")
}
if topNAggregation.SourceMeasure.Group == "" {
return errors.New("topNAggregation sourceMeasure group is empty")
}
if topNAggregation.CountersNumber <= 0 {
return errors.New("topNAggregation countersNumber is invalid")
}
if topNAggregation.FieldName == "" {
return errors.New("topNAggregation fieldName is empty")
}
return nil
}