Skip to content

Commit

Permalink
config: Allow loading of .regal.yaml file too (#1339)
Browse files Browse the repository at this point in the history
* config: Allow loading of .regal.yaml file too

Fixes #1288

Signed-off-by: Charlie Egan <[email protected]>

* config: Choose most specific file

Signed-off-by: Charlie Egan <[email protected]>

* Markdown line length lint

* / -> or

---------

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 authored Jan 15, 2025
1 parent faa9c52 commit 5986638
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 53 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ levels are available:
Additionally, some rules may have configuration options of their own. See the documentation page for a rule to learn
more about it.

**.regal/config.yaml**
**.regal/config.yaml** or **.regal.yaml**
```yaml
rules:
style:
Expand Down Expand Up @@ -400,12 +400,15 @@ project:
rego-version: 0
```
Regal will automatically search for a configuration file (`.regal/config.yaml`) in the current directory, and if not
found, traverse the parent directories either until either one is found, or the top of the directory hierarchy is
reached. If no configuration file is found, Regal will use the default configuration.
Regal will automatically search for a configuration file (`.regal/config.yaml`
or `.regal.yaml`) in the current directory, and if not found, traverse the
parent directories either until either one is found, or the top of the directory
hierarchy is reached. If no configuration file is found, Regal will use the
default configuration.

A custom configuration may be also be provided using the `--config-file`/`-c` option for `regal lint`, which when
provided will be used to override the default configuration.
A custom configuration may be also be provided using the `--config-file`/`-c`
option for `regal lint`, which when provided will be used to override the
default configuration.

## Ignoring Rules

Expand Down
2 changes: 1 addition & 1 deletion cmd/languageserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func init() {
} else {
fmt.Fprintf(
os.Stderr,
"Regal Language Server (path: %s, version: %s)",
"Regal Language Server (path: %s, version: %s)\n",
absPath,
cmp.Or(version.Version, "Unknown"),
)
Expand Down
2 changes: 1 addition & 1 deletion docs/remote-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ GitHub API rate limits when using Regal.

This functionality can be disabled in two ways:

* Using `.regal/config.yaml`: set `features.remote.check-version` to `false`.
* Using `.regal/config.yaml` / `.regal.yaml`: set `features.remote.check-version` to `false`.
* Using an environment variable: set `REGAL_DISABLE_CHECK_VERSION` to `true`.
2 changes: 1 addition & 1 deletion docs/rules/imports/use-rego-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ been disabled due to missing capabilities, kindly reminding you of them, but wit
In the example below we're using the capabilities setting to target OPA v0.55.0 (where `import rego.v1` is not
available):

**.regal/config.yaml**
**.regal/config.yaml** or **.regal.yaml**
```yaml
capabilities:
from:
Expand Down
3 changes: 3 additions & 0 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2526,7 +2526,10 @@ func (l *LanguageServer) handleInitialize(

configFile, err := config.FindConfig(workspaceRootPath)
if err == nil {
l.logf(log.LevelMessage, "using config file: %s", configFile.Name())
l.configWatcher.Watch(configFile.Name())
} else {
l.logf(log.LevelMessage, "no config file found in workspace: %s", err)
}

if _, err = l.loadWorkspaceContents(ctx, false); err != nil {
Expand Down
93 changes: 69 additions & 24 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,66 @@ type Builtin struct {
}

const (
regalDirName = ".regal"
configFileName = "config.yaml"
regalDirName = ".regal"
configFileName = "config.yaml"
standaloneConfigFileName = ".regal.yaml"
)

// FindRegalDirectory searches for a .regal directory first in the directory of path, and if not found,
// in the parent directory, and if not found, in the parent's parent directory, and so on.
func FindRegalDirectory(path string) (*os.File, error) {
// FindConfig attempts to find either the .regal directory or .regal.yaml
// config file, and returns the appropriate file or an error.
func FindConfig(path string) (*os.File, error) {
regalDir, regalDirError := FindRegalDirectory(path)
regalConfigFile, regalConfigFileError := FindRegalConfigFile(path)

var regalDirParent, regalConfigFileParent string
if regalDirError == nil && regalConfigFileError == nil {
regalDirParent = filepath.Dir(regalDir.Name())
regalConfigFileParent = filepath.Dir(regalConfigFile.Name())

if regalDirParent == regalConfigFileParent {
return nil, errors.New("conflicting config files: both .regal directory and .regal.yaml found")
}
}

if regalDirError != nil && regalConfigFileError != nil {
return nil, errors.New("could not find Regal config")
}

// if the config file parent is not "", then it was found, and if it's
// longer then it's more specific to the search path in question, and so we
// return that here in such cases.
if len(regalConfigFileParent) > len(regalDirParent) {
return regalConfigFile, nil
}

// if there is a .regal directory, when a config file is expected to be
// found inside.
if regalDirError == nil {
expectedConfigFilePath := filepath.Join(regalDir.Name(), rio.PathSeparator, configFileName)

_, err := os.Stat(expectedConfigFilePath)
if err != nil && os.IsNotExist(err) {
return nil, errors.New("config file was not found in .regal directory")
} else if err != nil {
return nil, fmt.Errorf("failed to stat .regal config file: %w", err)
}

return os.Open(expectedConfigFilePath) //nolint:wrapcheck
}

// regalConfigFileError is nil at this point, so we can return the file
return regalConfigFile, nil
}

// findUpwards searches for a file or directory matching the given name,
// starting from the provided path and moving upwards.
func findUpwards(path, name string, expectDir bool) (*os.File, error) {
finfo, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("failed to stat path %v: %w", path, err)
}

dir := path

if !finfo.IsDir() {
dir = filepath.Dir(path)
}
Expand All @@ -142,33 +188,31 @@ func FindRegalDirectory(path string) (*os.File, error) {
for {
var searchPath string
if volume == "" {
searchPath = filepath.Join(rio.PathSeparator, dir, regalDirName)
searchPath = filepath.Join(rio.PathSeparator, dir, name)
} else {
searchPath = filepath.Join(dir, regalDirName)
searchPath = filepath.Join(dir, name)
}

regalDir, err := os.Open(searchPath)
file, err := os.Open(searchPath)
if err == nil {
rdInfo, err := regalDir.Stat()
if err == nil && rdInfo.IsDir() {
return regalDir, nil
fileInfo, err := file.Stat()
if err == nil && fileInfo.IsDir() == expectDir {
return file, nil
}
}

if searchPath == volume+rio.PathSeparator+regalDirName {
if searchPath == volume+rio.PathSeparator+name {
// Stop traversing at the root path
return nil, fmt.Errorf("can't traverse past root directory %w", err)
}

// Move up one level in the directory tree
parts := strings.Split(dir, rio.PathSeparator)

if len(parts) < 2 {
return nil, errors.New("stopping as dir is root directory")
}

parts = parts[:len(parts)-1]

if parts[0] == volume {
parts[0] = volume + rio.PathSeparator
}
Expand All @@ -177,6 +221,16 @@ func FindRegalDirectory(path string) (*os.File, error) {
}
}

// FindRegalDirectory searches for a .regal directory upwards from the provided path.
func FindRegalDirectory(path string) (*os.File, error) {
return findUpwards(path, regalDirName, true)
}

// FindRegalConfigFile searches for a .regal.yaml config file upwards from the provided path.
func FindRegalConfigFile(path string) (*os.File, error) {
return findUpwards(path, standaloneConfigFileName, false)
}

// FindBundleRootDirectories finds all bundle root directories from the provided path,
// which **must** be an absolute path. Bundle root directories may be found either by:
//
Expand Down Expand Up @@ -285,15 +339,6 @@ func rootsFromRegalDirectory(regalDir *os.File) ([]string, error) {
return foundBundleRoots, nil
}

func FindConfig(path string) (*os.File, error) {
regalDir, err := FindRegalDirectory(path)
if err != nil {
return nil, fmt.Errorf("could not find .regal directory: %w", err)
}

return os.Open(filepath.Join(regalDir.Name(), rio.PathSeparator, configFileName)) //nolint:wrapcheck
}

func FromMap(confMap map[string]any) (Config, error) {
var conf Config

Expand Down
102 changes: 82 additions & 20 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"testing"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -54,32 +55,93 @@ func TestFindRegalDirectory(t *testing.T) {
func TestFindConfig(t *testing.T) {
t.Parallel()

fs := map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal/config.yaml": "",
testCases := map[string]struct {
FS map[string]string
Error string
ExpectedName string
}{
"no config file": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/bax.json": "",
},
Error: "could not find Regal config",
},
".regal/config.yaml": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal/config.yaml": "",
},
ExpectedName: "/foo/bar/.regal/config.yaml",
},
".regal/ dir missing config file": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal/.keep": "", // .keep file to ensure the dir is present
},
Error: "config file was not found in .regal directory",
},
".regal.yaml": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal.yaml": "",
},
ExpectedName: "/foo/bar/.regal.yaml",
},
".regal.yaml and .regal/config.yaml": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal.yaml": "",
"/foo/bar/.regal/config.yaml": "",
},
Error: "conflicting config files: both .regal directory and .regal.yaml found",
},
".regal.yaml with .regal/config.yaml at higher directory": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal.yaml": "",
"/.regal/config.yaml": "",
},
ExpectedName: "/foo/bar/.regal.yaml",
},
".regal/config.yaml with .regal.yaml at higher directory": {
FS: map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/.regal/config.yaml": "",
"/.regal.yaml": "",
},
ExpectedName: "/foo/bar/.regal/config.yaml",
},
}

test.WithTempFS(fs, func(root string) {
path := filepath.Join(root, "/foo/bar/baz")
for testName, testData := range testCases {
t.Run(testName, func(t *testing.T) {
t.Parallel()

if _, err := FindConfig(path); err != nil {
t.Error(err)
}
})
test.WithTempFS(testData.FS, func(root string) {
path := filepath.Join(root, "/foo/bar/baz")

fs = map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/bax.json": "",
}
configFile, err := FindConfig(path)
if testData.Error != "" {
if err == nil {
t.Fatalf("expected error %s, got nil", testData.Error)
}

test.WithTempFS(fs, func(root string) {
path := filepath.Join(root, "/foo/bar/baz")
if !strings.Contains(err.Error(), testData.Error) {
t.Fatalf("expected error %q, got %q", testData.Error, err.Error())
}
} else if err != nil {
t.Fatalf("expected no error, got %s", err)
}

_, err := FindConfig(path)
if err == nil {
t.Errorf("expected no config file to be found")
}
})
if testData.ExpectedName != "" {
if got, exp := strings.TrimPrefix(configFile.Name(), root), testData.ExpectedName; got != exp {
t.Fatalf("expected config file %q, got %q", exp, got)
}
}
})
})
}
}

func TestFindBundleRootDirectories(t *testing.T) {
Expand Down

0 comments on commit 5986638

Please sign in to comment.