| // 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 |
| } |