You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2024/04/28 04:38:37 UTC

(skywalking-banyandb) 01/01: Validate resource

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch metadata
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 361e4d3ebe1ecf99a7f8fd480882b370ec814e0f
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Sun Apr 28 12:35:01 2024 +0800

    Validate resource
    
    Signed-off-by: Gao Hongtao <ha...@gmail.com>
---
 CONTRIBUTING.md                              |   2 +-
 api/validate/validate.go                     | 243 +++++++++++++++++++++++++++
 banyand/measure/metadata.go                  |  21 +++
 banyand/measure/metadata_test.go             |   8 +-
 banyand/metadata/schema/group.go             |  15 ++
 banyand/metadata/schema/index.go             |  19 +++
 banyand/metadata/schema/measure.go           |  26 +++
 banyand/metadata/schema/property.go          |  23 ++-
 banyand/metadata/schema/stream.go            |  23 ++-
 banyand/metadata/schema/topn.go              |  10 ++
 banyand/metadata/schema/watcher_test.go      |  62 ++++++-
 banyand/stream/metadata.go                   |  17 ++
 docs/release.md                              |   2 +-
 ui/src/components/Aside/index.vue            |  14 +-
 ui/src/components/Editor/fieldsEditor.vue    |  12 --
 ui/src/components/Editor/tagEditor.vue       |   4 -
 ui/src/components/IndexRule/Editor.vue       |   4 -
 ui/src/components/TopNAggregation/Editor.vue |   4 -
 18 files changed, 468 insertions(+), 41 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d947479e..662e57a1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -75,7 +75,7 @@ Finally, run `make build` in the source directory, which will build the default
 make build
 ```
 
-Please refer to the [installation](./docs/installation/binaries.md) for more details.
+Please refer to the [installation](./docs/installation/binaries.md#Build-From-Source) for more details.
 
 Test your changes before submitting them by
 
diff --git a/api/validate/validate.go b/api/validate/validate.go
new file mode 100644
index 00000000..1c4481aa
--- /dev/null
+++ b/api/validate/validate.go
@@ -0,0 +1,243 @@
+// 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"
+
+	commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+	databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
+)
+
+// GroupForStreamOrMeasure validates the provided Group object for Stream or Measure.
+// It checks for nil values, empty strings, and unspecified enum values.
+func GroupForStreamOrMeasure(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")
+	}
+	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")
+	}
+	return tagFamily(measure.TagFamilies)
+}
+
+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 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
+}
diff --git a/banyand/measure/metadata.go b/banyand/measure/metadata.go
index dece4a1f..c940844a 100644
--- a/banyand/measure/metadata.go
+++ b/banyand/measure/metadata.go
@@ -29,6 +29,7 @@ import (
 	"github.com/apache/skywalking-banyandb/api/common"
 	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/api/validate"
 	"github.com/apache/skywalking-banyandb/banyand/internal/storage"
 	"github.com/apache/skywalking-banyandb/banyand/metadata"
 	"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
@@ -107,12 +108,20 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 		if g.Catalog != commonv1.Catalog_CATALOG_MEASURE {
 			return
 		}
+		if err := validate.GroupForStreamOrMeasure(g); err != nil {
+			sr.l.Warn().Err(err).Msg("group is ignored")
+			return
+		}
 		sr.SendMetadataEvent(resourceSchema.MetadataEvent{
 			Typ:      resourceSchema.EventAddOrUpdate,
 			Kind:     resourceSchema.EventKindGroup,
 			Metadata: g,
 		})
 	case schema.KindMeasure:
+		if err := validate.Measure(metadata.Spec.(*databasev1.Measure)); err != nil {
+			sr.l.Warn().Err(err).Msg("measure is ignored")
+			return
+		}
 		sr.SendMetadataEvent(resourceSchema.MetadataEvent{
 			Typ:      resourceSchema.EventAddOrUpdate,
 			Kind:     resourceSchema.EventKindResource,
@@ -124,6 +133,10 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 			sr.l.Warn().Msg("fail to convert message to IndexRuleBinding")
 			return
 		}
+		if err := validate.IndexRuleBinding(irb); err != nil {
+			sr.l.Warn().Err(err).Msg("index rule binding is ignored")
+			return
+		}
 		if irb.GetSubject().Catalog == commonv1.Catalog_CATALOG_MEASURE {
 			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			stm, err := sr.metadata.MeasureRegistry().GetMeasure(ctx, &commonv1.Metadata{
@@ -141,6 +154,10 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 			})
 		}
 	case schema.KindIndexRule:
+		if err := validate.IndexRule(metadata.Spec.(*databasev1.IndexRule)); err != nil {
+			sr.l.Warn().Err(err).Msg("index rule is ignored")
+			return
+		}
 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		defer cancel()
 		subjects, err := sr.metadata.Subjects(ctx, metadata.Spec.(*databasev1.IndexRule), commonv1.Catalog_CATALOG_MEASURE)
@@ -155,6 +172,10 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 			})
 		}
 	case schema.KindTopNAggregation:
+		if err := validate.TopNAggregation(metadata.Spec.(*databasev1.TopNAggregation)); err != nil {
+			sr.l.Warn().Err(err).Msg("topNAggregation is ignored")
+			return
+		}
 		sr.SendMetadataEvent(resourceSchema.MetadataEvent{
 			Typ:      resourceSchema.EventAddOrUpdate,
 			Kind:     resourceSchema.EventKindTopNAgg,
diff --git a/banyand/measure/metadata_test.go b/banyand/measure/metadata_test.go
index 808a123d..d07b091d 100644
--- a/banyand/measure/metadata_test.go
+++ b/banyand/measure/metadata_test.go
@@ -119,12 +119,12 @@ var _ = Describe("Metadata", func() {
 
 			It("should update a new measure", func() {
 				// Remove the first tag from the entity
-				measureSchema.Entity.TagNames = measureSchema.Entity.TagNames[1:]
-				entitySize := len(measureSchema.Entity.TagNames)
+				newEntityTag := measureSchema.Entity.TagNames[0] + "_updated"
+				measureSchema.Entity.TagNames[0] = newEntityTag
 
 				modRevision, err := svcs.metadataService.MeasureRegistry().UpdateMeasure(context.TODO(), measureSchema)
-				Expect(modRevision).ShouldNot(BeZero())
 				Expect(err).ShouldNot(HaveOccurred())
+				Expect(modRevision).ShouldNot(BeZero())
 
 				Eventually(func() bool {
 					val, err := svcs.measure.Measure(&commonv1.Metadata{
@@ -135,7 +135,7 @@ var _ = Describe("Metadata", func() {
 						return false
 					}
 
-					return len(val.GetSchema().GetEntity().TagNames) == entitySize
+					return newEntityTag == val.GetSchema().Entity.TagNames[0]
 				}).WithTimeout(flags.EventuallyTimeout).Should(BeTrue())
 			})
 		})
diff --git a/banyand/metadata/schema/group.go b/banyand/metadata/schema/group.go
index 0ef85e59..7040bb2b 100644
--- a/banyand/metadata/schema/group.go
+++ b/banyand/metadata/schema/group.go
@@ -76,6 +76,12 @@ func (e *etcdSchemaRegistry) CreateGroup(ctx context.Context, group *commonv1.Gr
 	if group.UpdatedAt != nil {
 		group.UpdatedAt = timestamppb.Now()
 	}
+	if group.Metadata == nil {
+		return errors.New("metadata is required")
+	}
+	if group.Metadata.Name == "" {
+		return errors.New("metadata.name is required")
+	}
 	_, err := e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind: KindGroup,
@@ -87,6 +93,15 @@ func (e *etcdSchemaRegistry) CreateGroup(ctx context.Context, group *commonv1.Gr
 }
 
 func (e *etcdSchemaRegistry) UpdateGroup(ctx context.Context, group *commonv1.Group) error {
+	if group.UpdatedAt != nil {
+		group.UpdatedAt = timestamppb.Now()
+	}
+	if group.Metadata == nil {
+		return errors.New("metadata is required")
+	}
+	if group.Metadata.Name == "" {
+		return errors.New("metadata.name is required")
+	}
 	_, err := e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind: KindGroup,
diff --git a/banyand/metadata/schema/index.go b/banyand/metadata/schema/index.go
index 41b1b284..45687c2d 100644
--- a/banyand/metadata/schema/index.go
+++ b/banyand/metadata/schema/index.go
@@ -25,6 +25,7 @@ import (
 
 	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/api/validate"
 )
 
 var (
@@ -59,6 +60,9 @@ func (e *etcdSchemaRegistry) CreateIndexRuleBinding(ctx context.Context, indexRu
 	if indexRuleBinding.UpdatedAt != nil {
 		indexRuleBinding.UpdatedAt = timestamppb.Now()
 	}
+	if err := validate.IndexRuleBinding(indexRuleBinding); err != nil {
+		return err
+	}
 	_, err := e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindIndexRuleBinding,
@@ -71,6 +75,12 @@ func (e *etcdSchemaRegistry) CreateIndexRuleBinding(ctx context.Context, indexRu
 }
 
 func (e *etcdSchemaRegistry) UpdateIndexRuleBinding(ctx context.Context, indexRuleBinding *databasev1.IndexRuleBinding) error {
+	if indexRuleBinding.UpdatedAt != nil {
+		indexRuleBinding.UpdatedAt = timestamppb.Now()
+	}
+	if err := validate.IndexRuleBinding(indexRuleBinding); err != nil {
+		return err
+	}
 	_, err := e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindIndexRuleBinding,
@@ -131,6 +141,9 @@ func (e *etcdSchemaRegistry) CreateIndexRule(ctx context.Context, indexRule *dat
 		buf = append(buf, indexRule.Metadata.Name...)
 		indexRule.Metadata.Id = crc32.ChecksumIEEE(buf)
 	}
+	if err := validate.IndexRule(indexRule); err != nil {
+		return err
+	}
 	_, err := e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindIndexRule,
@@ -150,6 +163,12 @@ func (e *etcdSchemaRegistry) UpdateIndexRule(ctx context.Context, indexRule *dat
 		}
 		indexRule.Metadata.Id = existingIndexRule.Metadata.Id
 	}
+	if indexRule.UpdatedAt != nil {
+		indexRule.UpdatedAt = timestamppb.Now()
+	}
+	if err := validate.IndexRule(indexRule); err != nil {
+		return err
+	}
 	_, err := e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindIndexRule,
diff --git a/banyand/metadata/schema/measure.go b/banyand/metadata/schema/measure.go
index a17f8fcd..76ec00ac 100644
--- a/banyand/metadata/schema/measure.go
+++ b/banyand/metadata/schema/measure.go
@@ -25,6 +25,7 @@ import (
 
 	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/api/validate"
 	"github.com/apache/skywalking-banyandb/pkg/timestamp"
 )
 
@@ -65,6 +66,17 @@ func (e *etcdSchemaRegistry) CreateMeasure(ctx context.Context, measure *databas
 			return 0, errors.Wrap(err, "interval is malformed")
 		}
 	}
+	if err := validate.Measure(measure); err != nil {
+		return 0, err
+	}
+	group := measure.Metadata.GetGroup()
+	g, err := e.GetGroup(ctx, group)
+	if err != nil {
+		return 0, err
+	}
+	if err := validate.GroupForStreamOrMeasure(g); err != nil {
+		return 0, err
+	}
 	return e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindMeasure,
@@ -76,11 +88,25 @@ func (e *etcdSchemaRegistry) CreateMeasure(ctx context.Context, measure *databas
 }
 
 func (e *etcdSchemaRegistry) UpdateMeasure(ctx context.Context, measure *databasev1.Measure) (int64, error) {
+	if measure.UpdatedAt != nil {
+		measure.UpdatedAt = timestamppb.Now()
+	}
 	if measure.GetInterval() != "" {
 		if _, err := timestamp.ParseDuration(measure.GetInterval()); err != nil {
 			return 0, errors.Wrap(err, "interval is malformed")
 		}
 	}
+	if err := validate.Measure(measure); err != nil {
+		return 0, err
+	}
+	group := measure.Metadata.GetGroup()
+	g, err := e.GetGroup(ctx, group)
+	if err != nil {
+		return 0, err
+	}
+	if err := validate.GroupForStreamOrMeasure(g); err != nil {
+		return 0, err
+	}
 	return e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:        KindMeasure,
diff --git a/banyand/metadata/schema/property.go b/banyand/metadata/schema/property.go
index 1f79597b..fae0e27c 100644
--- a/banyand/metadata/schema/property.go
+++ b/banyand/metadata/schema/property.go
@@ -110,10 +110,31 @@ func (e *etcdSchemaRegistry) ApplyProperty(ctx context.Context, property *proper
 		return false, 0, 0, ErrClosed
 	}
 	defer e.closer.Done()
+	if property.Metadata == nil {
+		return false, 0, 0, BadRequest("metadata", "metadata should not be nil")
+	}
+	if property.Metadata.Container == nil {
+		return false, 0, 0, BadRequest("metadata.container", "container should not be nil")
+	}
+	if property.Metadata.Container.Name == "" {
+		return false, 0, 0, BadRequest("metadata.container.name", "container.name should not be empty")
+	}
+	if property.Metadata.Id == "" {
+		return false, 0, 0, BadRequest("metadata.id", "id should not be empty")
+	}
+	if len(property.Tags) == 0 {
+		return false, 0, 0, BadRequest("tags", "tags should not be empty")
+	}
 	m := transformKey(property.GetMetadata())
 	group := m.GetGroup()
-	if _, getGroupErr := e.GetGroup(ctx, group); getGroupErr != nil {
+	if g, getGroupErr := e.GetGroup(ctx, group); getGroupErr != nil {
 		return false, 0, 0, errors.Wrap(getGroupErr, "group is not exist")
+	} else if g.Catalog != commonv1.Catalog_CATALOG_UNSPECIFIED {
+		return false, 0, 0, errors.New("group is not allowed to have properties")
+	}
+
+	if property.UpdatedAt != nil {
+		property.UpdatedAt = timestamppb.Now()
 	}
 	md := Metadata{
 		TypeMeta: TypeMeta{
diff --git a/banyand/metadata/schema/stream.go b/banyand/metadata/schema/stream.go
index 645a635d..81686ec7 100644
--- a/banyand/metadata/schema/stream.go
+++ b/banyand/metadata/schema/stream.go
@@ -24,6 +24,7 @@ import (
 
 	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/api/validate"
 )
 
 var streamKeyPrefix = "/streams/"
@@ -52,6 +53,20 @@ func (e *etcdSchemaRegistry) ListStream(ctx context.Context, opt ListOpt) ([]*da
 }
 
 func (e *etcdSchemaRegistry) UpdateStream(ctx context.Context, stream *databasev1.Stream) (int64, error) {
+	if stream.UpdatedAt != nil {
+		stream.UpdatedAt = timestamppb.Now()
+	}
+	if err := validate.Stream(stream); err != nil {
+		return 0, err
+	}
+	group := stream.Metadata.GetGroup()
+	g, err := e.GetGroup(ctx, group)
+	if err != nil {
+		return 0, err
+	}
+	if err := validate.GroupForStreamOrMeasure(g); err != nil {
+		return 0, err
+	}
 	return e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:        KindStream,
@@ -67,11 +82,17 @@ func (e *etcdSchemaRegistry) CreateStream(ctx context.Context, stream *databasev
 	if stream.UpdatedAt != nil {
 		stream.UpdatedAt = timestamppb.Now()
 	}
+	if err := validate.Stream(stream); err != nil {
+		return 0, err
+	}
 	group := stream.Metadata.GetGroup()
-	_, err := e.GetGroup(ctx, group)
+	g, err := e.GetGroup(ctx, group)
 	if err != nil {
 		return 0, err
 	}
+	if err := validate.GroupForStreamOrMeasure(g); err != nil {
+		return 0, err
+	}
 	return e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindStream,
diff --git a/banyand/metadata/schema/topn.go b/banyand/metadata/schema/topn.go
index 60597f1a..0eb4466f 100644
--- a/banyand/metadata/schema/topn.go
+++ b/banyand/metadata/schema/topn.go
@@ -24,6 +24,7 @@ import (
 
 	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/api/validate"
 )
 
 var topNAggregationKeyPrefix = "/topnagg/"
@@ -55,6 +56,9 @@ func (e *etcdSchemaRegistry) CreateTopNAggregation(ctx context.Context, topNAggr
 	if topNAggregation.UpdatedAt != nil {
 		topNAggregation.UpdatedAt = timestamppb.Now()
 	}
+	if err := validate.TopNAggregation(topNAggregation); err != nil {
+		return err
+	}
 	_, err := e.create(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindTopNAggregation,
@@ -67,6 +71,12 @@ func (e *etcdSchemaRegistry) CreateTopNAggregation(ctx context.Context, topNAggr
 }
 
 func (e *etcdSchemaRegistry) UpdateTopNAggregation(ctx context.Context, topNAggregation *databasev1.TopNAggregation) error {
+	if topNAggregation.UpdatedAt != nil {
+		topNAggregation.UpdatedAt = timestamppb.Now()
+	}
+	if err := validate.TopNAggregation(topNAggregation); err != nil {
+		return err
+	}
 	_, err := e.update(ctx, Metadata{
 		TypeMeta: TypeMeta{
 			Kind:  KindTopNAggregation,
diff --git a/banyand/metadata/schema/watcher_test.go b/banyand/metadata/schema/watcher_test.go
index 64ed142c..b7428fcc 100644
--- a/banyand/metadata/schema/watcher_test.go
+++ b/banyand/metadata/schema/watcher_test.go
@@ -116,6 +116,18 @@ var _ = ginkgo.Describe("Watcher", func() {
 			Metadata: &commonv1.Metadata{
 				Name: "testgroup-measure",
 			},
+			Catalog: commonv1.Catalog_CATALOG_MEASURE,
+			ResourceOpts: &commonv1.ResourceOpts{
+				ShardNum: 1,
+				SegmentInterval: &commonv1.IntervalRule{
+					Num:  1,
+					Unit: commonv1.IntervalRule_UNIT_DAY,
+				},
+				Ttl: &commonv1.IntervalRule{
+					Num:  3,
+					Unit: commonv1.IntervalRule_UNIT_DAY,
+				},
+			},
 		})
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
 		var modRevision int64
@@ -125,9 +137,31 @@ var _ = ginkgo.Describe("Watcher", func() {
 					Name:  fmt.Sprintf("testkey%d", i+1),
 					Group: "testgroup-measure",
 				},
+				Entity: &databasev1.Entity{
+					TagNames: []string{"testtag"},
+				},
+				TagFamilies: []*databasev1.TagFamilySpec{
+					{
+						Name: "testtagfamily",
+						Tags: []*databasev1.TagSpec{
+							{
+								Name: "testtag",
+								Type: databasev1.TagType_TAG_TYPE_STRING,
+							},
+						},
+					},
+				},
+				Fields: []*databasev1.FieldSpec{
+					{
+						Name:              "testfield",
+						FieldType:         databasev1.FieldType_FIELD_TYPE_INT,
+						EncodingMethod:    databasev1.EncodingMethod_ENCODING_METHOD_GORILLA,
+						CompressionMethod: databasev1.CompressionMethod_COMPRESSION_METHOD_ZSTD,
+					},
+				},
 			})
-			gomega.Expect(modRevision).ShouldNot(gomega.BeZero())
 			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			gomega.Expect(modRevision).ShouldNot(gomega.BeZero())
 		}
 
 		// Start the watcher
@@ -156,6 +190,18 @@ var _ = ginkgo.Describe("Watcher", func() {
 			Metadata: &commonv1.Metadata{
 				Name: "testgroup-stream",
 			},
+			Catalog: commonv1.Catalog_CATALOG_STREAM,
+			ResourceOpts: &commonv1.ResourceOpts{
+				ShardNum: 1,
+				SegmentInterval: &commonv1.IntervalRule{
+					Num:  1,
+					Unit: commonv1.IntervalRule_UNIT_DAY,
+				},
+				Ttl: &commonv1.IntervalRule{
+					Num:  3,
+					Unit: commonv1.IntervalRule_UNIT_DAY,
+				},
+			},
 		})
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
 		var modRevision int64
@@ -164,6 +210,20 @@ var _ = ginkgo.Describe("Watcher", func() {
 				Name:  "testkey",
 				Group: "testgroup-stream",
 			},
+			Entity: &databasev1.Entity{
+				TagNames: []string{"testtag"},
+			},
+			TagFamilies: []*databasev1.TagFamilySpec{
+				{
+					Name: "testtagfamily",
+					Tags: []*databasev1.TagSpec{
+						{
+							Name: "testtag",
+							Type: databasev1.TagType_TAG_TYPE_STRING,
+						},
+					},
+				},
+			},
 		})
 		gomega.Expect(modRevision).ShouldNot(gomega.BeZero())
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
diff --git a/banyand/stream/metadata.go b/banyand/stream/metadata.go
index 5e823902..fcc08065 100644
--- a/banyand/stream/metadata.go
+++ b/banyand/stream/metadata.go
@@ -29,6 +29,7 @@ import (
 	"github.com/apache/skywalking-banyandb/api/common"
 	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/api/validate"
 	"github.com/apache/skywalking-banyandb/banyand/internal/storage"
 	"github.com/apache/skywalking-banyandb/banyand/metadata"
 	"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
@@ -107,12 +108,20 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 		if g.Catalog != commonv1.Catalog_CATALOG_STREAM {
 			return
 		}
+		if err := validate.GroupForStreamOrMeasure(g); err != nil {
+			sr.l.Warn().Err(err).Msg("group is ignored")
+			return
+		}
 		sr.SendMetadataEvent(resourceSchema.MetadataEvent{
 			Typ:      resourceSchema.EventAddOrUpdate,
 			Kind:     resourceSchema.EventKindGroup,
 			Metadata: g,
 		})
 	case schema.KindStream:
+		if err := validate.Stream(metadata.Spec.(*databasev1.Stream)); err != nil {
+			sr.l.Warn().Err(err).Msg("stream is ignored")
+			return
+		}
 		sr.SendMetadataEvent(resourceSchema.MetadataEvent{
 			Typ:      resourceSchema.EventAddOrUpdate,
 			Kind:     resourceSchema.EventKindResource,
@@ -124,6 +133,10 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 			sr.l.Warn().Msg("fail to convert message to IndexRuleBinding")
 			return
 		}
+		if err := validate.IndexRuleBinding(irb); err != nil {
+			sr.l.Warn().Err(err).Msg("index rule binding is ignored")
+			return
+		}
 		if irb.GetSubject().Catalog == commonv1.Catalog_CATALOG_STREAM {
 			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			stm, err := sr.metadata.StreamRegistry().GetStream(ctx, &commonv1.Metadata{
@@ -141,6 +154,10 @@ func (sr *schemaRepo) OnAddOrUpdate(metadata schema.Metadata) {
 			})
 		}
 	case schema.KindIndexRule:
+		if err := validate.IndexRule(metadata.Spec.(*databasev1.IndexRule)); err != nil {
+			sr.l.Warn().Err(err).Msg("index rule is ignored")
+			return
+		}
 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		defer cancel()
 		subjects, err := sr.metadata.Subjects(ctx, metadata.Spec.(*databasev1.IndexRule), commonv1.Catalog_CATALOG_STREAM)
diff --git a/docs/release.md b/docs/release.md
index 3efc69c9..4201b70e 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -75,7 +75,7 @@ Keys to verify the Release Candidate :
 
 Guide to build the release from source :
 
- * https://github.com/apache/skywalking-banyandb/blob/v$VERSION/docs/installation/binaries.md
+ * https://github.com/apache/skywalking-banyandb/blob/v$VERSION/docs/installation/binaries.md#Build-From-Source
 
 Voting will start now and will remain open for at least 72 hours, all PMC members are required to give their votes.
 
diff --git a/ui/src/components/Aside/index.vue b/ui/src/components/Aside/index.vue
index 67e5adaf..ca4aa64a 100644
--- a/ui/src/components/Aside/index.vue
+++ b/ui/src/components/Aside/index.vue
@@ -59,10 +59,10 @@ const data = reactive({
         name: null,
         catalog: 'CATALOG_STREAM',
         shardNum: 1,
-        segmentIntervalUnit: "UNIT_UNSPECIFIED",
+        segmentIntervalUnit: "UNIT_DAY",
         segmentIntervalNum: 1,
-        ttlUnit: "UNIT_UNSPECIFIED",
-        ttlNum: 1
+        ttlUnit: "UNIT_DAY",
+        ttlNum: 3
     },
     activeMenu: '',
     search: '',
@@ -868,10 +868,10 @@ function clearGroupForm() {
         name: null,
         catalog: 'CATALOG_STREAM',
         shardNum: 1,
-        segmentIntervalUnit: "UNIT_UNSPECIFIED",
+        segmentIntervalUnit: "UNIT_DAY",
         segmentIntervalNum: 1,
-        ttlUnit: "UNIT_UNSPECIFIED",
-        ttlNum: 1
+        ttlUnit: "UNIT_DAY",
+        ttlNum: 3
     }
 }
 function initActiveMenu() {
@@ -1103,7 +1103,6 @@ initActiveMenu()
                 </el-form-item>
                 <el-form-item label="segment interval unit" :label-width="data.formLabelWidth" prop="segmentIntervalUnit">
                     <el-select v-model="data.groupForm.segmentIntervalUnit" placeholder="please select" style="width: 100%">
-                        <el-option label="UNIT_UNSPECIFIED" value="UNIT_UNSPECIFIED"></el-option>
                         <el-option label="UNIT_HOUR" value="UNIT_HOUR"></el-option>
                         <el-option label="UNIT_DAY" value="UNIT_DAY"></el-option>
                     </el-select>
@@ -1113,7 +1112,6 @@ initActiveMenu()
                 </el-form-item>
                 <el-form-item label="ttl unit" :label-width="data.formLabelWidth" prop="ttlUnit">
                     <el-select v-model="data.groupForm.ttlUnit" placeholder="please select" style="width: 100%">
-                        <el-option label="UNIT_UNSPECIFIED" value="UNIT_UNSPECIFIED"></el-option>
                         <el-option label="UNIT_HOUR" value="UNIT_HOUR"></el-option>
                         <el-option label="UNIT_DAY" value="UNIT_DAY"></el-option>
                     </el-select>
diff --git a/ui/src/components/Editor/fieldsEditor.vue b/ui/src/components/Editor/fieldsEditor.vue
index 2e525911..d1fe4b5b 100644
--- a/ui/src/components/Editor/fieldsEditor.vue
+++ b/ui/src/components/Editor/fieldsEditor.vue
@@ -37,10 +37,6 @@ const data = reactive({
 })
 
 const typeOptions = [
-    {
-        value: 'FIELD_TYPE_UNSPECIFIED',
-        label: 'UNSPECIFIED'
-    },
     {
         value: 'FIELD_TYPE_STRING',
         label: 'STRING'
@@ -59,20 +55,12 @@ const typeOptions = [
     }
 ]
 const encodingMethodOptions = [
-    {
-        value: 'ENCODING_METHOD_UNSPECIFIED',
-        label: 'UNSPECIFIED'
-    },
     {
         value: 'ENCODING_METHOD_GORILLA',
         label: 'GORILLA'
     }
 ]
 const compressionMethodOptions = [
-    {
-        value: 'COMPRESSION_METHOD_UNSPECIFIED',
-        label: 'UNSPECIFIED'
-    },
     {
         value: 'COMPRESSION_METHOD_ZSTD',
         label: 'ZSTD'
diff --git a/ui/src/components/Editor/tagEditor.vue b/ui/src/components/Editor/tagEditor.vue
index a4c9c84b..b5df7723 100644
--- a/ui/src/components/Editor/tagEditor.vue
+++ b/ui/src/components/Editor/tagEditor.vue
@@ -73,10 +73,6 @@ const typeOptions = [
     {
         value: 'TAG_TYPE_DATA_BINARY',
         label: 'DATA_BINARY'
-    },
-    {
-        value: 'TAG_TYPE_UNSPECIFIED',
-        label: 'UNSPECIFIED'
     }
 ]
 const validateTag = (rule: any, value: any, callback: any) => {
diff --git a/ui/src/components/IndexRule/Editor.vue b/ui/src/components/IndexRule/Editor.vue
index 3f932cc2..56e2543c 100644
--- a/ui/src/components/IndexRule/Editor.vue
+++ b/ui/src/components/IndexRule/Editor.vue
@@ -59,10 +59,6 @@ const rules = {
   ]
 }
 const typeList = [
-  {
-    label: "TYPE_UNSPECIFIED",
-    value: "TYPE_UNSPECIFIED"
-  },
   {
     label: "TYPE_INVERTED",
     value: "TYPE_INVERTED"
diff --git a/ui/src/components/TopNAggregation/Editor.vue b/ui/src/components/TopNAggregation/Editor.vue
index dfff02ed..149fa084 100644
--- a/ui/src/components/TopNAggregation/Editor.vue
+++ b/ui/src/components/TopNAggregation/Editor.vue
@@ -79,10 +79,6 @@ const rules = {
   ]
 }
 const fieldValueSortList = [
-  {
-    label: "SORT_UNSPECIFIED",
-    value: "SORT_UNSPECIFIED"
-  },
   {
     label: "SORT_DESC",
     value: "SORT_DESC"