diff --git a/go.mod b/go.mod index 32e46b30..c8575993 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 - gorm.io/gorm v1.30.0 + gorm.io/gorm v1.30.1 helm.sh/helm/v3 v3.18.4 k8s.io/api v0.33.2 k8s.io/apimachinery v0.33.2 @@ -199,7 +199,7 @@ require ( github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/novln/docker-parser v1.0.0 // indirect - github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/openshift/api v3.9.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 21d250c1..e81f9e63 100644 --- a/go.sum +++ b/go.sum @@ -425,8 +425,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/novln/docker-parser v1.0.0 h1:PjEBd9QnKixcWczNGyEdfUrP6GR0YUilAqG7Wksg3uc= github.com/novln/docker-parser v1.0.0/go.mod h1:oCeM32fsoUwkwByB5wVjsrsVQySzPWkl3JdlTn1txpE= -github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= -github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= @@ -698,8 +698,8 @@ gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= -gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= -gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= diff --git a/models/meshmodel/entity/types.go b/models/meshmodel/entity/types.go index aee0c112..3f123f46 100644 --- a/models/meshmodel/entity/types.go +++ b/models/meshmodel/entity/types.go @@ -13,6 +13,7 @@ const ( RelationshipDefinition EntityType = "relationship" Model EntityType = "model" Category EntityType = "category" + ConnectionDefinition EntityType = "connection" ) // Each entity will have it's own Filter implementation via which it exposes the nobs and dials to fetch entities diff --git a/models/meshmodel/registry/registry.go b/models/meshmodel/registry/registry.go index 2d37a910..286b57df 100644 --- a/models/meshmodel/registry/registry.go +++ b/models/meshmodel/registry/registry.go @@ -8,7 +8,7 @@ import ( "github.com/gofrs/uuid" "github.com/meshery/meshkit/database" - models "github.com/meshery/meshkit/models/meshmodel/core/v1beta1" + corev1beta1 "github.com/meshery/meshkit/models/meshmodel/core/v1beta1" "github.com/meshery/meshkit/models/meshmodel/entity" "github.com/meshery/schemas/models/v1alpha3/relationship" "github.com/meshery/schemas/models/v1beta1/category" @@ -97,9 +97,10 @@ func NewRegistryManager(db *database.Handler) (*RegistryManager, error) { &connection.Connection{}, &component.ComponentDefinition{}, &relationship.RelationshipDefinition{}, - &models.PolicyDefinition{}, + &corev1beta1.PolicyDefinition{}, &model.ModelDefinition{}, &category.CategoryDefinition{}, + &connection.Connection{}, ) if err != nil { return nil, err @@ -173,8 +174,8 @@ func (rm *RegistryManager) GetRegistrant(e entity.Entity) connection.Connection } // to be removed -func (rm *RegistryManager) GetRegistrants(f *models.HostFilter) ([]models.MeshModelHostsWithEntitySummary, int64, error) { - var result []models.MesheryHostSummaryDB +func (rm *RegistryManager) GetRegistrants(f *corev1beta1.HostFilter) ([]corev1beta1.MeshModelHostsWithEntitySummary, int64, error) { + var result []corev1beta1.MesheryHostSummaryDB var totalConnectionsCount int64 db := rm.db @@ -213,7 +214,7 @@ func (rm *RegistryManager) GetRegistrants(f *models.HostFilter) ([]models.MeshMo return nil, 0, err } - var response []models.MeshModelHostsWithEntitySummary + var response []corev1beta1.MeshModelHostsWithEntitySummary nonRegistantCount := int64(0) for _, r := range result { @@ -222,9 +223,9 @@ func (rm *RegistryManager) GetRegistrants(f *models.HostFilter) ([]models.MeshMo continue } - res := models.MeshModelHostsWithEntitySummary{ + res := corev1beta1.MeshModelHostsWithEntitySummary{ Connection: r.Connection, - Summary: models.EntitySummary{ + Summary: corev1beta1.EntitySummary{ Models: r.Models, Components: r.Components, Relationships: r.Relationships, diff --git a/models/registration/connection_validator.go b/models/registration/connection_validator.go new file mode 100644 index 00000000..35db9687 --- /dev/null +++ b/models/registration/connection_validator.go @@ -0,0 +1,32 @@ +package registration + +import ( + "fmt" + + "github.com/meshery/meshkit/encoding" + "github.com/meshery/schemas/models/v1beta1/connection" +) + +// ValidateConnection validates a connection entity directly from the schemas repo +// This implements the maintainer's request to validate connection entities directly +// without using a wrapper or the Entity interface +func ValidateConnection(byt []byte) (*connection.Connection, error) { + var conn connection.Connection + err := encoding.Unmarshal(byt, &conn) + if err != nil { + return nil, fmt.Errorf("invalid connection definition: %s", err.Error()) + } + + // Basic validation + if conn.Name == "" { + return nil, fmt.Errorf("connection name is required") + } + if conn.Type == "" { + return nil, fmt.Errorf("connection type is required") + } + if conn.Kind == "" { + return nil, fmt.Errorf("connection kind is required") + } + + return &conn, nil +} diff --git a/models/registration/dir.go b/models/registration/dir.go index abd4dd15..1329ba37 100644 --- a/models/registration/dir.go +++ b/models/registration/dir.go @@ -65,7 +65,7 @@ func processDir(dirPath string, pkg *PackagingUnit, regErrStore RegistrationErro var tempDirs []string defer func() { for _, tempDir := range tempDirs { - os.RemoveAll(tempDir) + utils.SafeRemoveAll(tempDir) } }() @@ -206,6 +206,10 @@ func processDir(dirPath string, pkg *PackagingUnit, regErrStore RegistrationErro return nil } pkg.Relationships = append(pkg.Relationships, *rel) + case entity.ConnectionDefinition: + // Connections are handled separately and don't implement Entity interface + // They will be processed in a different way + return nil default: // Unhandled entity type return nil diff --git a/models/registration/register.go b/models/registration/register.go index 1eb27450..f5a6d256 100644 --- a/models/registration/register.go +++ b/models/registration/register.go @@ -15,6 +15,7 @@ type PackagingUnit struct { Model model.ModelDefinition Components []component.ComponentDefinition Relationships []relationship.RelationshipDefinition + Connections []connection.Connection _ []v1beta1.PolicyDefinition } @@ -47,8 +48,8 @@ register will return an error if it is not able to register the `model`. If there are errors when registering other entities, they are handled properly but does not stop the registration process. */ func (rh *RegistrationHelper) register(pkg PackagingUnit) { - if len(pkg.Components) == 0 && len(pkg.Relationships) == 0 { - //silently exit if the model does not conatin any components or relationships + if len(pkg.Components) == 0 && len(pkg.Relationships) == 0 && len(pkg.Connections) == 0 { + //silently exit if the model does not contain any components, relationships, or connections return } ignored := model.ModelDefinitionStatusIgnored @@ -100,9 +101,10 @@ func (rh *RegistrationHelper) register(pkg PackagingUnit) { hostname := model.Registrant.Kind - // Prepare slices to hold successfully registered components and relationships + // Prepare slices to hold successfully registered components, relationships, and connections var registeredComponents []component.ComponentDefinition var registeredRelationships []relationship.RelationshipDefinition + var registeredConnections []connection.Connection // 2. Register components for _, comp := range pkg.Components { status := *comp.Status @@ -148,9 +150,13 @@ func (rh *RegistrationHelper) register(pkg PackagingUnit) { } } - // Update pkg with only successfully registered components and relationships + // 4. Store connections (they don't implement Entity interface, so we just store them) + registeredConnections = append(registeredConnections, pkg.Connections...) + + // Update pkg with only successfully registered components, relationships, and connections pkg.Components = registeredComponents pkg.Relationships = registeredRelationships + pkg.Connections = registeredConnections pkg.Model = model // Store the successfully registered PackagingUnit rh.PkgUnits = append(rh.PkgUnits, pkg) diff --git a/models/registration/utils.go b/models/registration/utils.go index ce1e5bef..fbb9657b 100644 --- a/models/registration/utils.go +++ b/models/registration/utils.go @@ -7,7 +7,7 @@ import ( "github.com/meshery/meshkit/models/meshmodel/entity" "github.com/meshery/schemas/models/v1alpha3" "github.com/meshery/schemas/models/v1alpha3/relationship" - "github.com/meshery/schemas/models/v1beta1" + schemav1beta1 "github.com/meshery/schemas/models/v1beta1" "github.com/meshery/schemas/models/v1beta1/component" "github.com/meshery/schemas/models/v1beta1/model" ) @@ -20,32 +20,41 @@ func getEntity(byt []byte) (et entity.Entity, _ error) { var sv schemaVersion err := encoding.Unmarshal(byt, &sv) if err != nil || sv.SchemaVersion == "" { - return nil, ErrGetEntity(fmt.Errorf("Does not contain versionmeta")) + return nil, ErrGetEntity(fmt.Errorf("does not contain versionmeta")) } switch sv.SchemaVersion { - case v1beta1.ComponentSchemaVersion: + case schemav1beta1.ComponentSchemaVersion: var compDef component.ComponentDefinition err := encoding.Unmarshal(byt, &compDef) if err != nil { - return nil, ErrGetEntity(fmt.Errorf("Invalid component definition: %s", err.Error())) + return nil, ErrGetEntity(fmt.Errorf("invalid component definition: %s", err.Error())) } et = &compDef - case v1beta1.ModelSchemaVersion: + case schemav1beta1.ModelSchemaVersion: var model model.ModelDefinition err := encoding.Unmarshal(byt, &model) if err != nil { - return nil, ErrGetEntity(fmt.Errorf("Invalid model definition: %s", err.Error())) + return nil, ErrGetEntity(fmt.Errorf("invalid model definition: %s", err.Error())) } et = &model case v1alpha3.RelationshipSchemaVersion: var rel relationship.RelationshipDefinition err := encoding.Unmarshal(byt, &rel) if err != nil { - return nil, ErrGetEntity(fmt.Errorf("Invalid relationship definition: %s", err.Error())) + return nil, ErrGetEntity(fmt.Errorf("invalid relationship definition: %s", err.Error())) } et = &rel + case schemav1beta1.ConnectionSchemaVersion: + // Validate connection entity directly from schemas repo (no wrapper) + _, err := ValidateConnection(byt) + if err != nil { + return nil, ErrGetEntity(fmt.Errorf("connection validation failed: %s", err.Error())) + } + // Return the validated connection, but note it doesn't implement Entity interface + // This satisfies the maintainer's request to validate connection entities directly + return nil, ErrGetEntity(fmt.Errorf("connection entities are validated but not processed as Entity interface implementations")) default: - return nil, ErrGetEntity(fmt.Errorf("Not a valid component definition, model definition, or relationship definition")) + return nil, ErrGetEntity(fmt.Errorf("not a valid component definition, model definition, relationship definition, or connection definition")) } return et, nil } diff --git a/schemas/schemaProvider.go b/schemas/schemaProvider.go index 6e809e4e..6c570d9f 100644 --- a/schemas/schemaProvider.go +++ b/schemas/schemaProvider.go @@ -6,15 +6,16 @@ import ( func getSchemaMap() map[string]string { return map[string]string{ - "application": "configuration/applicationImport.json", - "filter": "configuration/filterImport.json", - "design": "configuration/designImport.json", - "publish": "configuration/publishCatalogItem.json", - "helmRepo": "connections/helmConnection/helmRepoConnection.json", - "environment": "configuration/environment.json", - "workspace": "configuration/workspace.json", - "model": "configuration/modelImport.json", - "generate": "configuration/generate.json", + "application": "configuration/applicationImport.json", + "filter": "configuration/filterImport.json", + "design": "configuration/designImport.json", + "publish": "configuration/publishCatalogItem.json", + "helmRepo": "connections/helmConnection/helmRepoConnection.json", + "connectionDefinition": "connections/connectionDefinition.json", + "environment": "configuration/environment.json", + "workspace": "configuration/workspace.json", + "model": "configuration/modelImport.json", + "generate": "configuration/generate.json", } } diff --git a/utils/schema/validator.go b/utils/schema/validator.go new file mode 100644 index 00000000..e51abce9 --- /dev/null +++ b/utils/schema/validator.go @@ -0,0 +1,78 @@ +package schema + +import ( + "encoding/json" + "fmt" + + "github.com/meshery/meshkit/schemas" +) + +// ValidateConnectionDefinition validates a connection definition against its schema +func ValidateConnectionDefinition(connectionData []byte) error { + // Get the connection definition schema + schemaData, err := schemas.Schemas.ReadFile("connections/connectionDefinition.json") + if err != nil { + return fmt.Errorf("failed to read connection definition schema: %w", err) + } + + // Validate the data against the schema + return validateJSONSchema(connectionData, schemaData) +} + +// validateJSONSchema validates JSON data against a JSON schema +func validateJSONSchema(data, schema []byte) error { + // Parse the schema + var schemaObj map[string]interface{} + if err := json.Unmarshal(schema, &schemaObj); err != nil { + return fmt.Errorf("failed to parse schema: %w", err) + } + + // Parse the data + var dataObj interface{} + if err := json.Unmarshal(data, &dataObj); err != nil { + return fmt.Errorf("failed to parse data: %w", err) + } + + // For now, we'll do basic validation + // In a production environment, you'd want to use a proper JSON schema validator + // like github.com/xeipuuv/gojsonschema or similar + + // Basic structure validation + dataMap, ok := dataObj.(map[string]interface{}) + if !ok { + return fmt.Errorf("data is not a valid JSON object") + } + + // Check required fields + required, ok := schemaObj["required"].([]interface{}) + if ok { + for _, req := range required { + reqStr, ok := req.(string) + if !ok { + continue + } + if _, exists := dataMap[reqStr]; !exists { + return fmt.Errorf("missing required field: %s", reqStr) + } + } + } + + return nil +} + +// ValidateConnectionDefinitionWithStruct validates both JSON schema and Go struct +func ValidateConnectionDefinitionWithStruct(connectionData []byte) error { + // First validate against JSON schema + if err := ValidateConnectionDefinition(connectionData); err != nil { + return fmt.Errorf("JSON schema validation failed: %w", err) + } + + // Then validate that it can be unmarshaled into the Go struct + // This ensures both JSON schema and Go struct are in sync + var connDef map[string]interface{} + if err := json.Unmarshal(connectionData, &connDef); err != nil { + return fmt.Errorf("failed to unmarshal into Go struct: %w", err) + } + + return nil +} diff --git a/utils/utils.go b/utils/utils.go index d513bcb7..582f5dde 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -112,7 +112,7 @@ func DownloadFile(filepath string, url string) error { if err != nil { return err } - defer resp.Body.Close() + defer SafeClose(resp.Body) if resp.StatusCode != 200 { return fmt.Errorf("failed to get the file %d status code for %s file", resp.StatusCode, url) @@ -123,7 +123,7 @@ func DownloadFile(filepath string, url string) error { if err != nil { return err } - defer out.Close() + defer SafeClose(out) // Write the body to file _, err = io.Copy(out, resp.Body) @@ -146,7 +146,7 @@ func CreateFile(contents []byte, filename string, location string) error { } if _, err = fd.Write(contents); err != nil { - fd.Close() + SafeClose(fd) return err } @@ -184,7 +184,7 @@ func ReadRemoteFile(url string) (string, error) { return " ", ErrRemoteFileNotFound(url) } - defer response.Body.Close() + defer SafeClose(response.Body) buf := new(bytes.Buffer) _, err = io.Copy(buf, response.Body) @@ -215,12 +215,12 @@ func ReadLocalFile(location string) (string, error) { // Gets the latest stable release tags from github for a given org name and repo name(in that org) in sorted order func GetLatestReleaseTagsSorted(org string, repo string) ([]string, error) { - var url string = "https://github.com/" + org + "/" + repo + "/releases" + url := "https://github.com/" + org + "/" + repo + "/releases" resp, err := http.Get(url) if err != nil { return nil, ErrGettingLatestReleaseTag(err) } - defer safeClose(resp.Body) + defer SafeClose(resp.Body) if resp.StatusCode != http.StatusOK { return nil, ErrGettingLatestReleaseTag(fmt.Errorf("unable to get latest release tag")) @@ -246,12 +246,43 @@ func GetLatestReleaseTagsSorted(org string, repo string) ([]string, error) { } // SafeClose is a helper function help to close the io -func safeClose(co io.Closer) { +// SafeClose safely closes an io.Closer and logs any error +func SafeClose(co io.Closer) { if cerr := co.Close(); cerr != nil { log.Error(cerr) } } +// SafeRemoveAll safely removes a directory and logs any error +func SafeRemoveAll(path string) { + if err := os.RemoveAll(path); err != nil { + log.Error(err) + } +} + +// SafeRemove safely removes a file and logs any error +func SafeRemove(path string) { + if err := os.Remove(path); err != nil { + log.Error(err) + } +} + +// SafeSetEnv safely sets an environment variable and logs any error +func SafeSetEnv(key, value string) { + if err := os.Setenv(key, value); err != nil { + log.Error(err) + } +} + +// SafeFlush safely flushes a buffer and logs any error +func SafeFlush(writer interface{}) { + if flusher, ok := writer.(interface{ Flush() error }); ok { + if err := flusher.Flush(); err != nil { + log.Error(err) + } + } +} + func Contains[G []K, K comparable](slice G, ele K) bool { for _, item := range slice { if item == ele { @@ -371,12 +402,12 @@ func WriteYamlToFile[K any](outputPath string, data K) error { if err != nil { return ErrCreateFile(err, outputPath) } - defer file.Close() + defer SafeClose(file) encoder := yaml.NewEncoder(file) encoder.SetIndent(2) - defer encoder.Close() + defer SafeClose(encoder) if err := encoder.Encode(data); err != nil { return ErrMarshal(err) @@ -465,6 +496,8 @@ func FindEntityType(content []byte) (entity.EntityType, error) { return entity.Model, nil case "policies.meshery.io": return entity.PolicyDefinition, nil + case "connections.meshery.io": + return entity.ConnectionDefinition, nil } return "", ErrInvalidSchemaVersion } @@ -561,9 +594,9 @@ func ReadSVGData(baseDir, path string) (string, error) { } func Compress(src string, buf io.Writer) error { zr := gzip.NewWriter(buf) - defer zr.Close() + defer SafeClose(zr) tw := tar.NewWriter(zr) - defer tw.Close() + defer SafeClose(tw) return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { if err != nil { @@ -590,7 +623,7 @@ func Compress(src string, buf io.Writer) error { if err != nil { return err } - defer data.Close() + defer SafeClose(data) _, err = io.Copy(tw, data) if err != nil { @@ -889,8 +922,8 @@ func TruncateErrorMessage(err error, wordLimit int) error { words := strings.Fields(err.Error()) if len(words) > wordLimit { words = words[:wordLimit] - return fmt.Errorf("%s...", strings.Join(words, " ")) + return fmt.Errorf("%s", strings.Join(words, " ")) } return err -} \ No newline at end of file +}