diff --git a/README.md b/README.md index 6580669..2155ff1 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,58 @@ PASS (0.00158s) modelsource/MyFirstModule/DomainModels$DomainModel.yaml ![Mendix Lint report](./resources/lint-xunit-report.png) Lint Mendix Yaml files. This tool checks for common mistakes and enforces best practices. It uses OPA as policy engine. Therefore policies must be written in the powerful Rego language. Please refer to [Rego language reference](https://www.openpolicyagent.org/docs/latest/policy-reference/) for more information on the syntax and semantics. +## watch + +Watch for changes in the model and lint the changes. + +``` +./bin/mxlint-darwin-arm64 watch --input resources/app/ --rules resources/rules +FILE "triggered event" CREATE [-] +INFO[0000] Watching for changes in /Users/xcheng/private/git/mxlint-cli/resources/app +INFO[0000] Output directory: modelsource +INFO[0000] Rules directory: resources/rules +INFO[0000] Mode: basic +INFO[0000] Exporting resources/app/App.mpr to modelsource +INFO[0000] Transforming microflow UpdateUserHelper +INFO[0000] Transforming microflow AssertTrue +INFO[0000] Transforming microflow CreateUserIfNotExists +INFO[0000] Transforming microflow AssertTrue_2 +INFO[0000] Transforming microflow ChangeMyPassword +INFO[0000] Transforming microflow ShowMyPasswordForm +INFO[0000] Transforming microflow ManageMyAccount +INFO[0000] Loop detected; not traversing +INFO[0000] Transforming microflow NewAccount +INFO[0000] Transforming microflow ChangePassword +INFO[0000] Transforming microflow NewWebServiceAccount +INFO[0000] Transforming microflow ShowPasswordForm +INFO[0000] Transforming microflow SaveNewAccount +INFO[0000] Transforming microflow MicroflowSplit +INFO[0000] Transforming microflow MicroflowSimple +INFO[0000] Transforming microflow MicroflowComplexSplit +INFO[0000] Loop detected; not traversing +INFO[0000] Transforming microflow MicroflowLoopNested +INFO[0000] Loop detected; not traversing +INFO[0000] Loop detected; not traversing +INFO[0000] Loop detected; not traversing +INFO[0000] Loop detected; not traversing +INFO[0000] Transforming microflow MicroflowSplitThenMerge +INFO[0000] Loop detected; not traversing +INFO[0000] Transforming microflow MicroflowLoop +INFO[0000] Loop detected; not traversing +INFO[0000] Loop detected; not traversing +INFO[0000] Transforming microflow MyFirstLogic +INFO[0000] Transforming microflow MicroflowForLoop +INFO[0000] Transforming microflow VA_Age +INFO[0000] Found 361 documents +INFO[0000] Completed resources/app/App.mpr +## resources/rules/001_0003_security_checks.rego +FAIL (0.00171s) modelsource/Security$ProjectSecurity.yaml + +WARN[0000] Rule resources/rules/001_0003_security_checks.rego: 1 failures +WARN[0000] Document modelsource/Security$ProjectSecurity.yaml: [HIGH, Security, 4099] Security check is not enabled in Project Security +WARN[0000] Lint failed: 1 failures +``` + ### Features - Export Mendix model to Yaml diff --git a/cmd/mxlint/main.go b/cmd/mxlint/main.go index 15a5cf5..292c805 100644 --- a/cmd/mxlint/main.go +++ b/cmd/mxlint/main.go @@ -3,9 +3,12 @@ package main import ( "fmt" "os" + "path/filepath" + "time" "github.com/cinaq/mendix-cli/lint" "github.com/cinaq/mendix-cli/mpr" + "github.com/radovskyb/watcher" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -78,6 +81,76 @@ func main() { cmdLint.Flags().Bool("verbose", false, "Turn on for debug logs") rootCmd.AddCommand(cmdLint) + var cmdWatch = &cobra.Command{ + Use: "watch", + Short: "Watch for changes in the model, export-model and lint continuously", + Long: "Continuous linting of the model. This is useful when you are developing your application and want to be notified of any changes that might break the rules.", + Run: func(cmd *cobra.Command, args []string) { + inputDirectory, _ := cmd.Flags().GetString("input") + outputDirectory, _ := cmd.Flags().GetString("output") + mode, _ := cmd.Flags().GetString("mode") + rulesDirectory, _ := cmd.Flags().GetString("rules") + + w := watcher.New() + w.IgnoreHiddenFiles(true) + + log := logrus.New() + log.SetLevel(logrus.InfoLevel) + + mpr.SetLogger(log) + lint.SetLogger(log) + + expandedPath, err := filepath.Abs(inputDirectory) + if err != nil { + log.Fatalln(err) + } + + go func() { + for { + select { + case event := <-w.Event: + fmt.Println(event) + + log.Infof("Watching for changes in %s", expandedPath) + log.Infof("Output directory: %s", outputDirectory) + log.Infof("Rules directory: %s", rulesDirectory) + log.Infof("Mode: %s", mode) + mpr.ExportModel(inputDirectory, outputDirectory, false, mode) + err := lint.EvalAll(rulesDirectory, outputDirectory, "", "") + if err != nil { + log.Warningf("Lint failed: %s", err) + } + case err := <-w.Error: + log.Fatalln(err) + case <-w.Closed: + return + } + } + }() + + if err := w.AddRecursive(inputDirectory); err != nil { + log.Fatalln(err) + } + w.Ignore(outputDirectory) + + // first run + go func() { + w.Wait() + w.TriggerEvent(watcher.Create, nil) + }() + + if err := w.Start(time.Millisecond * 100); err != nil { + log.Fatalln(err) + } + }, + } + + cmdWatch.Flags().StringP("input", "i", ".", "Path to directory or mpr file to export. If it's a directory, all mpr files will be exported") + cmdWatch.Flags().StringP("output", "o", "modelsource", "Path to directory to write the yaml files. If it doesn't exist, it will be created") + cmdWatch.Flags().StringP("mode", "m", "basic", "Export mode. Valid options: basic, advanced") + cmdWatch.Flags().StringP("rules", "r", "rules", "Path to directory with rules") + rootCmd.AddCommand(cmdWatch) + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/go.mod b/go.mod index 6ba481b..0744c70 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/glebarez/go-sqlite v1.22.0 github.com/open-policy-agent/opa v0.62.1 + github.com/radovskyb/watcher v1.0.7 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 go.mongodb.org/mongo-driver v1.14.0 diff --git a/go.sum b/go.sum index 5742f64..83eeb63 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/lint/lint.go b/lint/lint.go index 95c5cbb..3e69f80 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -75,8 +75,21 @@ func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonF } } + for _, ts := range testsuites { + if ts.Failures > 0 { + log.Warningf("Rule %s: %d failures", ts.Name, ts.Failures) + for _, tc := range ts.Testcases { + if tc.Failure != nil { + log.Warningf(" Document %s: %s", tc.Name, tc.Failure.Message) + } + } + } + } + if failuresCount > 0 { return fmt.Errorf("%d failures", failuresCount) + } else { + log.Infof("All good my friend") } return nil } @@ -180,7 +193,7 @@ func evalTestcase(rulePath string, queryString string, inputFilePath string) (*T if !result { myErrors := make([]string, 0) for _, err := range errors { - log.Warnf("Rule failed: %s", err) + //log.Warnf("Rule failed: %s", err) myErrors = append(myErrors, fmt.Sprintf("%s", err)) } failure = &Failure{