diff --git a/go.mod b/go.mod index bb542625..fc7cff55 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,7 @@ replace ( // github.com/docker/libcompose => github.com/docker/libcompose v0.4.1-0.20190808084053-143e0f3f1ab9 github.com/googleapis/gnostic => github.com/googleapis/gnostic v0.5.5 github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305 - -//github.com/meshery/schemas v0.8.34 => ../schemas + github.com/meshery/schemas => ../schemas ) require ( @@ -204,7 +203,7 @@ require ( github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.11.1 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -309,3 +308,5 @@ require ( ) replace github.com/meshery/meshkit => ../meshkit + +replace github.com/meshery/schemas => ../schemas diff --git a/go.sum b/go.sum index 02052f4f..ad79384c 100644 --- a/go.sum +++ b/go.sum @@ -409,8 +409,9 @@ github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJ github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= +github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= @@ -428,8 +429,6 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/meshery/meshery-operator v0.8.11 h1:eDo2Sw0jjVrXsvvhF8LenADM58pK+7Z68ROPVIejTPc= github.com/meshery/meshery-operator v0.8.11/go.mod h1:hQEtFKKa5Fr/Mskk6bV5ip3bQ0+3F0u1voYS3XisBp4= -github.com/meshery/schemas v0.8.113 h1:gQMKIJSKGTT6PuJje0XTxuHfCv9ERVLX9IrG1JyFAM8= -github.com/meshery/schemas v0.8.113/go.mod h1:UwoDPg3j/2BMfytdgaalpemRKUxB9HhFsUZqJA01/G4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= diff --git a/models/meshmodel/registry/error.go b/models/meshmodel/registry/error.go index 7bb40e69..9d47d477 100644 --- a/models/meshmodel/registry/error.go +++ b/models/meshmodel/registry/error.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/meshery/meshkit/errors" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" ) var ( @@ -18,15 +19,7 @@ var ( ) func ErrGetById(err error, id string) error { - return errors.New( - ErrUnknownHostCode, - errors.Alert, - []string{"Failed to get the entity with the given ID: " + id}, - []string{err.Error()}, - []string{"Entity with the given ID may not be present in the registry", "Registry might be inaccessible at the moment"}, - []string{"Check if your ID is correct", "If the registry is inaccesible, please try again after some time"}, - ) - + return registryerrors.ErrGetById(err, id) } func ErrUnknownHost(err error) error { diff --git a/models/meshmodel/registry/errors/error.go b/models/meshmodel/registry/errors/error.go new file mode 100644 index 00000000..3d2b3fdc --- /dev/null +++ b/models/meshmodel/registry/errors/error.go @@ -0,0 +1,20 @@ +package errors + +import ( + meshkiterrors "github.com/meshery/meshkit/errors" +) + +const ( + ErrGetByIdCode = "meshkit-11262" +) + +func ErrGetById(err error, id string) error { + return meshkiterrors.New( + ErrGetByIdCode, + meshkiterrors.Alert, + []string{"Failed to get the entity with the given ID: " + id}, + []string{err.Error()}, + []string{"Entity with the given ID may not be present in the registry", "Registry might be inaccessible at the moment"}, + []string{"Check if your ID is correct", "If the registry is inaccesible, please try again after some time"}, + ) +} diff --git a/models/meshmodel/registry/registry.go b/models/meshmodel/registry/registry.go index 2d37a910..73037a48 100644 --- a/models/meshmodel/registry/registry.go +++ b/models/meshmodel/registry/registry.go @@ -10,9 +10,11 @@ import ( "github.com/meshery/meshkit/database" models "github.com/meshery/meshkit/models/meshmodel/core/v1beta1" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/schemas/models/v1alpha3/relationship" + regv1alpha3 "github.com/meshery/meshkit/models/meshmodel/registry/v1alpha3" + regv1beta1 "github.com/meshery/meshkit/models/meshmodel/registry/v1beta1" + schemarelationship "github.com/meshery/schemas/models/v1alpha3/relationship" "github.com/meshery/schemas/models/v1beta1/category" - "github.com/meshery/schemas/models/v1beta1/component" + schemacomponent "github.com/meshery/schemas/models/v1beta1/component" "github.com/meshery/schemas/models/v1beta1/connection" "github.com/meshery/schemas/models/v1beta1/model" "golang.org/x/text/cases" @@ -95,8 +97,8 @@ func NewRegistryManager(db *database.Handler) (*RegistryManager, error) { err := rm.db.AutoMigrate( &Registry{}, &connection.Connection{}, - &component.ComponentDefinition{}, - &relationship.RelationshipDefinition{}, + &schemacomponent.ComponentDefinition{}, + &schemarelationship.RelationshipDefinition{}, &models.PolicyDefinition{}, &model.ModelDefinition{}, &category.CategoryDefinition{}, @@ -106,14 +108,27 @@ func NewRegistryManager(db *database.Handler) (*RegistryManager, error) { } return &rm, nil } + +func (rm *RegistryManager) GetComponentSummary( + f *schemacomponent.ComponentSummaryFilter, +) (*schemacomponent.ComponentSummary, error) { + return regv1beta1.GetSummary(f, rm.db) +} + +func (rm *RegistryManager) GetRelationshipSummary( + f *schemarelationship.RelationshipSummaryFilter, +) (*schemarelationship.RelationshipSummary, error) { + return regv1alpha3.GetSummary(f, rm.db) +} + func (rm *RegistryManager) Cleanup() { _ = rm.db.Migrator().DropTable( &Registry{}, &connection.Connection{}, - &component.ComponentDefinition{}, + &schemacomponent.ComponentDefinition{}, &model.ModelDefinition{}, &category.CategoryDefinition{}, - &relationship.RelationshipDefinition{}, + &schemarelationship.RelationshipDefinition{}, ) } func (rm *RegistryManager) RegisterEntity(h connection.Connection, en entity.Entity) (bool, bool, error) { diff --git a/models/meshmodel/registry/v1alpha3/relationship_filter.go b/models/meshmodel/registry/v1alpha3/relationship_filter.go index f74be193..ef8882c4 100644 --- a/models/meshmodel/registry/v1alpha3/relationship_filter.go +++ b/models/meshmodel/registry/v1alpha3/relationship_filter.go @@ -3,7 +3,7 @@ package v1alpha3 import ( "github.com/meshery/meshkit/database" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/meshkit/models/meshmodel/registry" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" "github.com/meshery/schemas/models/v1alpha3/relationship" "gorm.io/gorm/clause" @@ -39,7 +39,7 @@ func (rf *RelationshipFilter) GetById(db *database.Handler) (entity.Entity, erro r := &relationship.RelationshipDefinition{} err := db.First(r, "id = ?", rf.Id).Error if err != nil { - return nil, registry.ErrGetById(err, rf.Id) + return nil, registryerrors.ErrGetById(err, rf.Id) } return r, err } diff --git a/models/meshmodel/registry/v1alpha3/relationship_summary.go b/models/meshmodel/registry/v1alpha3/relationship_summary.go new file mode 100644 index 00000000..dcc78611 --- /dev/null +++ b/models/meshmodel/registry/v1alpha3/relationship_summary.go @@ -0,0 +1,146 @@ +package v1alpha3 + +import ( + "fmt" + "slices" + + "github.com/meshery/meshkit/database" + relationship "github.com/meshery/schemas/models/v1alpha3/relationship" + + "gorm.io/gorm" +) + +func GetSummary(relationshipFilter *relationship.RelationshipSummaryFilter, db *database.Handler) (*relationship.RelationshipSummary, error) { + if err := validate(relationshipFilter); err != nil { + return nil, err + } + + summary := &relationship.RelationshipSummary{} + + base := db.Table("relationship_definition_dbs"). + Joins("JOIN model_dbs ON relationship_definition_dbs.model_id = model_dbs.id"). + Joins("JOIN category_dbs ON model_dbs.category_id = category_dbs.id") + + status := "enabled" + + if relationshipFilter.Status != nil { + status = *relationshipFilter.Status + } + + base = base.Where("model_dbs.status = ?", status) + + if relationshipFilter.Kind != nil { + greedy := relationshipFilter.Greedy != nil && *relationshipFilter.Greedy + if greedy { + base = base.Where("relationship_definition_dbs.kind LIKE ?", "%"+*relationshipFilter.Kind+"%") + } else { + base = base.Where("relationship_definition_dbs.kind = ?", *relationshipFilter.Kind) + } + } + + if relationshipFilter.RelationshipType != nil { + base = base.Where("relationship_definition_dbs.type = ?", *relationshipFilter.RelationshipType) + } + + if relationshipFilter.SubType != nil { + base = base.Where("relationship_definition_dbs.sub_type = ?", *relationshipFilter.SubType) + } + if relationshipFilter.ModelName != nil { + base = base.Where("model_dbs.name = ?", *relationshipFilter.ModelName) + } + if relationshipFilter.Version != nil { + base = base.Where("model_dbs.model->>'version' = ?", *relationshipFilter.Version) + } + if err := base.Session(&gorm.Session{}). + Distinct("relationship_definition_dbs.id"). + Count(&summary.Total).Error; err != nil { + return nil, err + } + + shouldCompute := func(dim relationship.RelationshipSummaryFilterInclude) bool { + if relationshipFilter.Include == nil || len(*relationshipFilter.Include) == 0 { + return true + } + + return slices.Contains(*relationshipFilter.Include, dim) + } + + type groupEntry = struct { + Count int32 `json:"count" yaml:"count"` + Key string `json:"key" yaml:"key"` + } + + type dimensionInfo struct { + dim relationship.RelationshipSummaryFilterInclude + selectExpr string + groupExpr string + setRows func([]groupEntry) + } + + dimensions := []dimensionInfo{ + { + dim: relationship.ByModel, + selectExpr: "model_dbs.name as Key, COUNT(DISTINCT(relationship_definition_dbs.id)) as Count", + groupExpr: "model_dbs.name", + setRows: func(rows []groupEntry) { + summary.ByModel = &rows + }, + }, + { + dim: relationship.ByKind, + selectExpr: "relationship_definition_dbs.kind as Key, COUNT(DISTINCT(relationship_definition_dbs.id)) as Count", + groupExpr: "relationship_definition_dbs.kind", + setRows: func(rows []groupEntry) { + summary.ByKind = &rows + }, + }, + { + dim: relationship.ByType, + selectExpr: "relationship_definition_dbs.type as Key, COUNT(DISTINCT(relationship_definition_dbs.id)) as Count", + groupExpr: "relationship_definition_dbs.type", + setRows: func(rows []groupEntry) { + summary.ByType = &rows + }, + }, + { + dim: relationship.BySubtype, + selectExpr: "relationship_definition_dbs.sub_type as Key, COUNT(DISTINCT(relationship_definition_dbs.id)) as Count", + groupExpr: "relationship_definition_dbs.sub_type", + setRows: func(rows []groupEntry) { + summary.BySubType = &rows + }, + }, + } + + for _, d := range dimensions { + if shouldCompute(d.dim) { + var rows []groupEntry + err := base.Session(&gorm.Session{}). + Select(d.selectExpr). + Group(d.groupExpr). + Scan(&rows).Error + if err != nil { + return nil, err + } + d.setRows(rows) + } + } + return summary, nil +} + +func validate(relationshipFilter *relationship.RelationshipSummaryFilter) error { + if relationshipFilter == nil { + return fmt.Errorf("nil relationship summary filter") + } + + if relationshipFilter.Include == nil { + return nil + } + + for _, dim := range *relationshipFilter.Include { + if !dim.Valid() { + return fmt.Errorf("unknown include dimension %s", dim) + } + } + return nil +} diff --git a/models/meshmodel/registry/v1alpha3/relationship_summary_test.go b/models/meshmodel/registry/v1alpha3/relationship_summary_test.go new file mode 100644 index 00000000..848e469f --- /dev/null +++ b/models/meshmodel/registry/v1alpha3/relationship_summary_test.go @@ -0,0 +1,142 @@ +package v1alpha3 + +import ( + "path/filepath" + "testing" + + "github.com/gofrs/uuid" + "github.com/meshery/meshkit/database" + "github.com/meshery/schemas/models/v1alpha3/relationship" + "github.com/meshery/schemas/models/v1beta1/category" + "github.com/meshery/schemas/models/v1beta1/model" + "github.com/stretchr/testify/require" +) + +type relationshipSummaryRecord struct { + Id uuid.UUID `gorm:"column:id;primaryKey"` + Kind string `gorm:"column:kind"` + RelationshipType string `gorm:"column:type"` + SubType string `gorm:"column:sub_type"` + Status *relationship.RelationshipDefinitionStatus `gorm:"column:status"` + ModelId uuid.UUID `gorm:"column:model_id"` + Version string `gorm:"column:version"` +} + +func (relationshipSummaryRecord) TableName() string { + return "relationship_definition_dbs" +} + +func TestRelationshipSummary_GetSummary(t *testing.T) { + db := newRelationshipSummaryTestDB(t) + seedRelationshipSummaryData(t, db) + + include := []relationship.RelationshipSummaryFilterInclude{relationship.ByModel, relationship.ByKind} + f := &relationship.RelationshipSummaryFilter{Include: &include} + + summary, err := GetSummary(f, db) + require.NoError(t, err) + require.Equal(t, int64(2), summary.Total) + require.Equal(t, map[string]int32{"model-a": 1, "model-b": 1}, relationshipGroupToMap(summary.ByModel)) + require.Equal(t, map[string]int32{"edge": 1, "hierarchy": 1}, relationshipGroupToMap(summary.ByKind)) + require.Empty(t, summary.ByType) + require.Empty(t, summary.BySubType) +} + +func TestRelationshipSummary_Validate(t *testing.T) { + include := []relationship.RelationshipSummaryFilterInclude{relationship.RelationshipSummaryFilterInclude("invalid_dimension")} + f := &relationship.RelationshipSummaryFilter{Include: &include} + _, err := GetSummary(f, newRelationshipSummaryTestDB(t)) + require.Error(t, err) +} + +func newRelationshipSummaryTestDB(t *testing.T) *database.Handler { + t.Helper() + + h, err := database.New(database.Options{ + Engine: database.SQLITE, + Filename: filepath.Join(t.TempDir(), "relationship-summary.db"), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = h.DBClose() + }) + + err = h.AutoMigrate( + &category.CategoryDefinition{}, + &model.ModelDefinition{}, + &relationshipSummaryRecord{}, + ) + require.NoError(t, err) + + return &h +} + +func seedRelationshipSummaryData(t *testing.T, db *database.Handler) { + t.Helper() + + cat := category.CategoryDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "infra", + } + require.NoError(t, db.Create(&cat).Error) + + modelStatus := model.ModelDefinitionStatus("enabled") + modelA := model.ModelDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "model-a", + DisplayName: "Model A", + Status: modelStatus, + CategoryId: cat.Id, + Model: struct { + Version string `json:"version" yaml:"version"` + }{Version: "v1.0.0"}, + } + modelB := model.ModelDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "model-b", + DisplayName: "Model B", + Status: modelStatus, + CategoryId: cat.Id, + Model: struct { + Version string `json:"version" yaml:"version"` + }{Version: "v1.0.0"}, + } + require.NoError(t, db.Create(&modelA).Error) + require.NoError(t, db.Create(&modelB).Error) + + relationshipStatus := relationship.RelationshipDefinitionStatus("enabled") + rel1 := relationshipSummaryRecord{ + Id: uuid.Must(uuid.NewV4()), + Kind: "edge", + RelationshipType: "binding", + SubType: "parent", + Status: &relationshipStatus, + ModelId: modelA.Id, + Version: "v1.0.0", + } + rel2 := relationshipSummaryRecord{ + Id: uuid.Must(uuid.NewV4()), + Kind: "hierarchy", + RelationshipType: "binding", + SubType: "child", + Status: &relationshipStatus, + ModelId: modelB.Id, + Version: "v1.0.0", + } + require.NoError(t, db.Create(&rel1).Error) + require.NoError(t, db.Create(&rel2).Error) +} + +func relationshipGroupToMap(rows *[]struct { + Count int32 `json:"count" yaml:"count"` + Key string `json:"key" yaml:"key"` +}) map[string]int32 { + if rows == nil { + return map[string]int32{} + } + out := make(map[string]int32, len(*rows)) + for _, row := range *rows { + out[row.Key] = row.Count + } + return out +} diff --git a/models/meshmodel/registry/v1beta1/category_filter.go b/models/meshmodel/registry/v1beta1/category_filter.go index e0964605..2a5f504a 100644 --- a/models/meshmodel/registry/v1beta1/category_filter.go +++ b/models/meshmodel/registry/v1beta1/category_filter.go @@ -3,7 +3,7 @@ package v1beta1 import ( "github.com/meshery/meshkit/database" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/meshkit/models/meshmodel/registry" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" "github.com/meshery/schemas/models/v1beta1/category" "gorm.io/gorm/clause" ) @@ -30,7 +30,7 @@ func (cf *CategoryFilter) GetById(db *database.Handler) (entity.Entity, error) { c := &category.CategoryDefinition{} err := db.First(c, "id = ?", cf.Id).Error if err != nil { - return nil, registry.ErrGetById(err, cf.Id) + return nil, registryerrors.ErrGetById(err, cf.Id) } return c, err } diff --git a/models/meshmodel/registry/v1beta1/component_filter.go b/models/meshmodel/registry/v1beta1/component_filter.go index f2c39ac0..cc603c0b 100644 --- a/models/meshmodel/registry/v1beta1/component_filter.go +++ b/models/meshmodel/registry/v1beta1/component_filter.go @@ -3,7 +3,7 @@ package v1beta1 import ( "github.com/meshery/meshkit/database" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/meshkit/models/meshmodel/registry" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" "github.com/meshery/schemas/models/v1beta1/category" "github.com/meshery/schemas/models/v1beta1/component" "github.com/meshery/schemas/models/v1beta1/connection" @@ -40,7 +40,7 @@ func (cf *ComponentFilter) GetById(db *database.Handler) (entity.Entity, error) c := &component.ComponentDefinition{} err := db.First(c, "id = ?", cf.Id).Error if err != nil { - return nil, registry.ErrGetById(err, cf.Id) + return nil, registryerrors.ErrGetById(err, cf.Id) } return c, err } @@ -73,11 +73,11 @@ func (componentFilter *ComponentFilter) Get(db *database.Handler) ([]entity.Enti Joins("JOIN connections ON connections.id = model_dbs.connection_id") componentStatus := "enabled" - if componentFilter.Status != "" { - componentStatus = componentFilter.Status - } - finder = finder.Where("component_definition_dbs.status = ?", componentStatus) - + if componentFilter.Status != "" { + componentStatus = componentFilter.Status + } + finder = finder.Where("component_definition_dbs.status = ?", componentStatus) + if componentFilter.Greedy { if componentFilter.Name != "" && componentFilter.DisplayName != "" { finder = finder.Where("component_definition_dbs.component->>'kind' LIKE ? OR display_name LIKE ?", "%"+componentFilter.Name+"%", componentFilter.DisplayName+"%") diff --git a/models/meshmodel/registry/v1beta1/component_summary.go b/models/meshmodel/registry/v1beta1/component_summary.go new file mode 100644 index 00000000..aa64400b --- /dev/null +++ b/models/meshmodel/registry/v1beta1/component_summary.go @@ -0,0 +1,138 @@ +package v1beta1 + +import ( + "fmt" + "slices" + + "github.com/meshery/meshkit/database" + "github.com/meshery/schemas/models/v1beta1/component" + "gorm.io/gorm" +) + +func GetSummary(componentFilter *component.ComponentSummaryFilter, db *database.Handler) (*component.ComponentSummary, error) { + if err := validate(componentFilter); err != nil { + return nil, err + } + + summary := &component.ComponentSummary{} + base := db.Model(&component.ComponentDefinition{}). + Joins("JOIN model_dbs ON component_definition_dbs.model_id = model_dbs.id"). + Joins("JOIN category_dbs ON model_dbs.category_id = category_dbs.id"). + Joins("JOIN connections ON connections.id = model_dbs.connection_id") + componentStatus := "enabled" + if componentFilter.Status != nil { + componentStatus = *componentFilter.Status + } + base = base.Where("component_definition_dbs.status = ?", componentStatus) + if componentFilter.Annotations != nil { + switch *componentFilter.Annotations { + case component.True: + base = base.Where("component_definition_dbs.metadata->>'isAnnotation' = true") + case component.False: + base = base.Where("component_definition_dbs.metadata->>'isAnnotation' = false") + } + } + + if componentFilter.ModelName != nil && *componentFilter.ModelName != "all" { + base = base.Where("model_dbs.name = ?", *componentFilter.ModelName) + } + + if componentFilter.CategoryName != nil { + base = base.Where("category_dbs.name = ?", *componentFilter.CategoryName) + } + if componentFilter.Version != nil { + base = base.Where("model_dbs.model->>'version' = ?", *componentFilter.Version) + } + if componentFilter.Registrant != nil { + base = base.Where("connections.name = ?", *componentFilter.Registrant) + } + + if err := base.Session(&gorm.Session{}). + Distinct("component_definition_dbs.id"). + Count(&summary.Total).Error; err != nil { + return nil, err + } + + type groupEntry = struct { + Count int32 `json:"count" yaml:"count"` + Key string `json:"key" yaml:"key"` + } + + shouldCompute := func(dim component.ComponentSummaryFilterInclude) bool { + if componentFilter.Include == nil || len(*componentFilter.Include) == 0 { + return true + } + return slices.Contains(*componentFilter.Include, dim) + } + + type dimensionInfo struct { + dim component.ComponentSummaryFilterInclude + selectExpr string + groupExpr string + setRows func([]groupEntry) + } + + dimensions := []dimensionInfo{ + { + dim: component.ByModel, + selectExpr: "model_dbs.name as Key, COUNT(DISTINCT(component_definition_dbs.id)) as Count", + groupExpr: "model_dbs.name", + setRows: func(rows []groupEntry) { + summary.ByModel = &rows + }, + }, + { + dim: component.ByCategory, + selectExpr: "category_dbs.name as Key, COUNT(DISTINCT(component_definition_dbs.id)) as Count", + groupExpr: "category_dbs.name", + setRows: func(rows []groupEntry) { + summary.ByCategory = &rows + }, + }, + { + dim: component.ByRegistrant, + selectExpr: "connections.name as Key, COUNT(DISTINCT(component_definition_dbs.id)) as Count", + groupExpr: "connections.name", + setRows: func(rows []groupEntry) { + summary.ByRegistrant = &rows + }, + }, + } + + for _, d := range dimensions { + if shouldCompute(d.dim) { + var rows []groupEntry + err := base.Session(&gorm.Session{}). + Select(d.selectExpr). + Group(d.groupExpr). + Scan(&rows).Error + if err != nil { + return nil, err + } + d.setRows(rows) + } + } + + return summary, nil +} + +func validate(componentFilter *component.ComponentSummaryFilter) error { + if componentFilter == nil { + return fmt.Errorf("nil component summary filter") + } + + if componentFilter.Annotations != nil && !componentFilter.Annotations.Valid() { + return fmt.Errorf("unknown annotations value %s", *componentFilter.Annotations) + } + + if componentFilter.Include == nil { + return nil + } + + for _, dim := range *componentFilter.Include { + if !dim.Valid() { + return fmt.Errorf("unknown include dimension %s", dim) + } + } + return nil +} diff --git a/models/meshmodel/registry/v1beta1/component_summary_test.go b/models/meshmodel/registry/v1beta1/component_summary_test.go new file mode 100644 index 00000000..28a11a91 --- /dev/null +++ b/models/meshmodel/registry/v1beta1/component_summary_test.go @@ -0,0 +1,150 @@ +package v1beta1 + +import ( + "path/filepath" + "testing" + + "github.com/gofrs/uuid" + "github.com/meshery/meshkit/database" + "github.com/meshery/schemas/models/v1beta1/category" + "github.com/meshery/schemas/models/v1beta1/component" + "github.com/meshery/schemas/models/v1beta1/connection" + "github.com/meshery/schemas/models/v1beta1/model" + "github.com/stretchr/testify/require" +) + +func TestComponentSummary_GetSummary(t *testing.T) { + db := newComponentSummaryTestDB(t) + seedComponentSummaryData(t, db) + + include := []component.ComponentSummaryFilterInclude{component.ByModel, component.ByCategory, component.ByRegistrant} + f := &component.ComponentSummaryFilter{Include: &include} + + summary, err := GetSummary(f, db) + require.NoError(t, err) + require.Equal(t, int64(3), summary.Total) + + require.Equal(t, map[string]int32{"model-a": 2, "model-b": 1}, componentGroupToMap(summary.ByModel)) + require.Equal(t, map[string]int32{"infra": 3}, componentGroupToMap(summary.ByCategory)) + require.Equal(t, map[string]int32{"registrant-a": 2, "registrant-b": 1}, componentGroupToMap(summary.ByRegistrant)) +} + +func TestComponentSummary_Validate(t *testing.T) { + include := []component.ComponentSummaryFilterInclude{component.ComponentSummaryFilterInclude("unknown_dimension")} + f := &component.ComponentSummaryFilter{Include: &include} + _, err := GetSummary(f, newComponentSummaryTestDB(t)) + require.Error(t, err) +} + +func newComponentSummaryTestDB(t *testing.T) *database.Handler { + t.Helper() + + h, err := database.New(database.Options{ + Engine: database.SQLITE, + Filename: filepath.Join(t.TempDir(), "component-summary.db"), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = h.DBClose() + }) + + err = h.AutoMigrate( + &connection.Connection{}, + &category.CategoryDefinition{}, + &model.ModelDefinition{}, + &component.ComponentDefinition{}, + ) + require.NoError(t, err) + + return &h +} + +func seedComponentSummaryData(t *testing.T, db *database.Handler) { + t.Helper() + + connA := connection.Connection{ID: uuid.Must(uuid.NewV4()), Name: "registrant-a"} + connB := connection.Connection{ID: uuid.Must(uuid.NewV4()), Name: "registrant-b"} + require.NoError(t, db.Create(&connA).Error) + require.NoError(t, db.Create(&connB).Error) + + cat := category.CategoryDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "infra", + } + require.NoError(t, db.Create(&cat).Error) + + modelStatus := model.ModelDefinitionStatus("enabled") + modelA := model.ModelDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "model-a", + DisplayName: "Model A", + Status: modelStatus, + CategoryId: cat.Id, + RegistrantId: connA.ID, + Model: struct { + Version string `json:"version" yaml:"version"` + }{Version: "v1.0.0"}, + } + modelB := model.ModelDefinition{ + Id: uuid.Must(uuid.NewV4()), + Name: "model-b", + DisplayName: "Model B", + Status: modelStatus, + CategoryId: cat.Id, + RegistrantId: connB.ID, + Model: struct { + Version string `json:"version" yaml:"version"` + }{Version: "v1.0.0"}, + } + require.NoError(t, db.Create(&modelA).Error) + require.NoError(t, db.Create(&modelB).Error) + + componentStatus := component.ComponentDefinitionStatus("enabled") + comp1 := component.ComponentDefinition{ + Id: uuid.Must(uuid.NewV4()), + DisplayName: "comp-1", + Status: &componentStatus, + ModelId: modelA.Id, + Component: component.Component{ + Kind: "Deployment", + Version: "v1", + }, + } + comp2 := component.ComponentDefinition{ + Id: uuid.Must(uuid.NewV4()), + DisplayName: "comp-2", + Status: &componentStatus, + ModelId: modelA.Id, + Component: component.Component{ + Kind: "Service", + Version: "v1", + }, + } + comp3 := component.ComponentDefinition{ + Id: uuid.Must(uuid.NewV4()), + DisplayName: "comp-3", + Status: &componentStatus, + ModelId: modelB.Id, + Component: component.Component{ + Kind: "Pod", + Version: "v1", + }, + } + require.NoError(t, db.Create(&comp1).Error) + require.NoError(t, db.Create(&comp2).Error) + require.NoError(t, db.Create(&comp3).Error) +} + +func componentGroupToMap(rows *[]struct { + Count int32 `json:"count" yaml:"count"` + Key string `json:"key" yaml:"key"` +}) map[string]int32 { + if rows == nil { + return map[string]int32{} + } + out := make(map[string]int32, len(*rows)) + for _, row := range *rows { + out[row.Key] = row.Count + } + return out +} diff --git a/models/meshmodel/registry/v1beta1/model_filter.go b/models/meshmodel/registry/v1beta1/model_filter.go index eda466f4..02b9a0e5 100644 --- a/models/meshmodel/registry/v1beta1/model_filter.go +++ b/models/meshmodel/registry/v1beta1/model_filter.go @@ -3,7 +3,7 @@ package v1beta1 import ( "github.com/meshery/meshkit/database" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/meshkit/models/meshmodel/registry" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" "github.com/meshery/schemas/models/v1alpha3/relationship" "github.com/meshery/schemas/models/v1beta1/component" "github.com/meshery/schemas/models/v1beta1/model" @@ -58,7 +58,7 @@ func (mf *ModelFilter) GetById(db *database.Handler) (entity.Entity, error) { // Retrieve the model by ID err := db.First(m, "id = ?", mf.Id).Error if err != nil { - return nil, registry.ErrGetById(err, mf.Id) + return nil, registryerrors.ErrGetById(err, mf.Id) } // Include components if requested diff --git a/models/meshmodel/registry/v1beta1/policy_filter.go b/models/meshmodel/registry/v1beta1/policy_filter.go index b83edb6d..ad6c5d42 100644 --- a/models/meshmodel/registry/v1beta1/policy_filter.go +++ b/models/meshmodel/registry/v1beta1/policy_filter.go @@ -4,7 +4,7 @@ import ( "github.com/meshery/meshkit/database" "github.com/meshery/meshkit/models/meshmodel/core/v1beta1" "github.com/meshery/meshkit/models/meshmodel/entity" - "github.com/meshery/meshkit/models/meshmodel/registry" + registryerrors "github.com/meshery/meshkit/models/meshmodel/registry/errors" ) type PolicyFilter struct { @@ -29,7 +29,7 @@ func (pf *PolicyFilter) GetById(db *database.Handler) (entity.Entity, error) { p := &v1beta1.PolicyDefinition{} err := db.First(p, "id = ?", pf.Id).Error if err != nil { - return nil, registry.ErrGetById(err, pf.Id) + return nil, registryerrors.ErrGetById(err, pf.Id) } return p, err }