Skip to content

Commit

Permalink
config: Allow loading of .regal.yaml file too
Browse files Browse the repository at this point in the history
Fixes StyraInc#1288

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 committed Jan 15, 2025
1 parent faa9c52 commit 7323f17
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 45 deletions.
80 changes: 56 additions & 24 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,53 @@ 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)

// if both are found, then we must error as this is a misconfiguration
if regalDirError == nil && regalConfigFileError == nil {
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 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
}

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 +175,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 +208,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 +326,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
79 changes: 58 additions & 21 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,68 @@ 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
}{
"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": "",
},
},
".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": "",
},
},
".regal.yaml and .regal/config": {
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",
},
}

test.WithTempFS(fs, func(root string) {
path := filepath.Join(root, "/foo/bar/baz")

if _, err := FindConfig(path); err != nil {
t.Error(err)
}
})
for testName, testData := range testCases {
t.Run(testName, func(t *testing.T) {
t.Parallel()

fs = map[string]string{
"/foo/bar/baz/p.rego": "",
"/foo/bar/bax.json": "",
}
test.WithTempFS(testData.FS, func(root string) {
path := filepath.Join(root, "/foo/bar/baz")

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

_, err := FindConfig(path)
if err == nil {
t.Errorf("expected no config file to be found")
}
})
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)
}
})
})
}
}

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

0 comments on commit 7323f17

Please sign in to comment.