diff --git a/.golangci.yaml b/.golangci.yaml index 1b2bde4f..61bb1907 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -19,6 +19,7 @@ linters: - varnamelen - nonamedreturns - testpackage + - gochecknoinits - gomnd - godox - exhaustruct diff --git a/cmd/lint.go b/cmd/lint.go new file mode 100644 index 00000000..dbd14a3f --- /dev/null +++ b/cmd/lint.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "context" + "embed" + "errors" + "fmt" + "io/fs" + "log" + "os" + "time" + + "github.com/open-policy-agent/opa/loader" + "github.com/spf13/cobra" + rio "github.com/styrainc/regal/internal/io" + "github.com/styrainc/regal/pkg/config" + "github.com/styrainc/regal/pkg/linter" +) + +type lintCommandParams struct { + timeout time.Duration +} + +//nolint:gochecknoglobals +var EmbedBundleFS embed.FS + +var errNoFileProvided = errors.New("at least one file or directory must be provided for linting") + +func init() { + params := lintCommandParams{} + + lintCommand := &cobra.Command{ + Use: "lint [path [...]]", + Short: "Lint Rego source files", + Long: `Lint Rego source files for linter rule violations.`, + + PreRunE: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return errNoFileProvided + } + + return nil + }, + + Run: func(_ *cobra.Command, args []string) { + if err := lint(args, params); err != nil { + log.SetOutput(os.Stderr) + log.Println(err) + os.Exit(1) + } + }, + } + + lintCommand.Flags().DurationVar(¶ms.timeout, "timeout", 0, "set timeout for linting (default unlimited)") + + RootCommand.AddCommand(lintCommand) +} + +func lint(args []string, params lintCommandParams) error { + ctx := context.Background() + + if params.timeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, params.timeout) + + defer cancel() + } + + // Create new fs from root of bundle, to avoid having to deal with + // "bundle" in paths (i.e. `data.bundle.regal`) + bfs, err := fs.Sub(EmbedBundleFS, "bundle") + if err != nil { + return fmt.Errorf("failed reading embedded bundle %w", err) + } + + regalRules := rio.MustLoadRegalBundleFS(bfs) + + policies, err := loader.AllRegos(args) + if err != nil { + return fmt.Errorf("failed to load policy from provided args %w", err) + } + + // TODO: Allow user-provided path to config + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get cwd %w", err) + } + + regal := linter.NewLinter().WithAddedBundle(regalRules) + + userConfig, err := config.FindConfig(cwd) + if err == nil { + defer rio.CloseFileIgnore(userConfig) + + regal = regal.WithUserConfig(rio.MustYAMLToMap(userConfig)) + } + + rep, err := regal.Lint(ctx, policies) + if err != nil { + return fmt.Errorf("error(s) ecountered while linting %w", err) + } + + // TODO: Create reporter interface and implementations + log.Println(rep) + + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..3c72c6c3 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "os" + "path" + + "github.com/spf13/cobra" +) + +// RootCommand is the base CLI command that all subcommands are added to. +// +//nolint:gochecknoglobals +var RootCommand = &cobra.Command{ + Use: path.Base(os.Args[0]), + Short: "Regal", + Long: "Regal is a linter for Rego, with the goal of making your Rego magnificent!", +} diff --git a/go.mod b/go.mod index 5e8714a0..f7283c9b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,10 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index 6f5a4c13..e11ed8ec 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -26,6 +27,9 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -41,7 +45,12 @@ github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= diff --git a/main.go b/main.go index 3ea739e5..c113cb64 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,11 @@ package main import ( - "context" "embed" - "io/fs" "log" "os" - "time" - "github.com/open-policy-agent/opa/loader" - rio "github.com/styrainc/regal/internal/io" - "github.com/styrainc/regal/pkg/config" - "github.com/styrainc/regal/pkg/linter" + "github.com/styrainc/regal/cmd" ) // Note: this will bundle the tests as well, but since that has negligible impact on the size of the binary, @@ -22,52 +16,14 @@ var bundle embed.FS func main() { // Remove date and time from any `log.*` calls, as that doesn't add much of value here + // Evaluate options for logging later.. log.SetFlags(0) - if len(os.Args) < 2 { - log.Fatal("At least one file or directory must be provided for linting") - } - - // Create new fs from root of bundle, do avoid having to deal with - // "bundle" in paths (i.e. `data.bundle.regal`) - bfs, err := fs.Sub(bundle, "bundle") - if err != nil { - log.Fatal(err) - } - - regalRules := rio.MustLoadRegalBundleFS(bfs) - - policies, err := loader.AllRegos(os.Args[1:]) - if err != nil { - log.Fatal(err) - } - - // TODO: Allow user-provided path to config - cwd, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - - // TODO: Make timeout configurable via command line flag - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(5)*time.Second) - defer cancel() - - regal := linter.NewLinter().WithAddedBundle(regalRules) - - userConfig, err := config.FindConfig(cwd) - if err == nil { - defer rio.CloseFileIgnore(userConfig) - - regal = regal.WithUserConfig(rio.MustYAMLToMap(userConfig)) - } + // The embedded FS can't point to parent directories, so while not pretty, we'll + // need to pass it from here to the next command + cmd.EmbedBundleFS = bundle - rep, err := regal.Lint(ctx, policies) - if err != nil { - defer func() { - log.Fatal(err) - }() - } else { - // TODO: Create reporter interface and implementations - log.Println(rep) + if err := cmd.RootCommand.Execute(); err != nil { + os.Exit(1) } }