diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index e064c89ae..b9781d1e7 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -19,10 +19,21 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: ${{ github.event.pull_request.commits }} + - name: setup go + uses: actions/setup-go@v6 + with: + go-version: "1.25.x" + cache: false + - name: validate conventional commit prefix working-directory: scripts run: ./validate-conventional-commit-prefix.sh + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + args: --timeout=5m --color=always --max-same-issues=0 --max-issues-per-linter=0 + - name: setup regal uses: StyraInc/setup-regal@v1 with: @@ -46,7 +57,13 @@ jobs: --format github validate: - runs-on: ubuntu-latest + strategy: + matrix: + os: + # - 'ubuntu-latest' + # - 'macos-latest' + - 'windows-latest' + runs-on: ${{ matrix.os }} steps: - name: checkout source uses: actions/checkout@v5 @@ -57,11 +74,6 @@ jobs: go-version: "1.25.x" cache: false - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - args: --timeout=5m --color=always --max-same-issues=0 --max-issues-per-linter=0 - - name: build run: make build @@ -86,12 +98,27 @@ jobs: detik-install: false file-install: false - - name: test examples + - name: test examples (nix) + if: ${{ matrix.os != 'windows-latest' }} run: make test-examples - - name: acceptance + - name: test examples (windows) + if: ${{ matrix.os == 'windows-latest' }} + run: | + $env:Path += ";C:\Users\runneradmin\.local\share\bats\bin" + make test-examples + + - name: acceptance (nix) + if: ${{ matrix.os != 'windows-latest' }} run: make test-acceptance + # TODO: Uncomment as a part of https://github.com/open-policy-agent/conftest/issues/1203 + # - name: acceptance (windows) + # if: ${{ matrix.os == 'windows-latest' }} + # run: | + # $env:Path += ";C:\Users\runneradmin\.local\share\bats\bin" + # make test-acceptance + - name: test oci push/pull run: ./scripts/push-pull-e2e.sh diff --git a/go.mod b/go.mod index 67b75f56e..8f7696239 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/open-policy-agent/conftest -go 1.25.0 +go 1.25.3 require ( cuelang.org/go v0.15.0 @@ -31,6 +31,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/subosito/gotenv v1.6.0 github.com/tmccombs/hcl2json v0.6.7 + github.com/tzrikka/xdg v1.3.2 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 google.golang.org/protobuf v1.36.9 olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 diff --git a/go.sum b/go.sum index f9dbd9b0d..a3f771872 100644 --- a/go.sum +++ b/go.sum @@ -417,6 +417,8 @@ github.com/tmccombs/hcl2json v0.6.7 h1:RYKTs4kd/gzRsEiv7J3M2WQ7TYRYZVc+0H0pZdERk github.com/tmccombs/hcl2json v0.6.7/go.mod h1:lJgBOOGDpbhjvdG2dLaWsqB4KBzul2HytfDTS3H465o= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/tzrikka/xdg v1.3.2 h1:KhTyYLq58F0vs+Vw9uLMKw44qfEZDW5/ZQyCgTE9P2k= +github.com/tzrikka/xdg v1.3.2/go.mod h1:qZP8butzr5W+sv0kS17GKvBnoUgKVty9fBo+W8LsnTY= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index 259c61cc9..5b98427ff 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -3,8 +3,6 @@ package registry import ( "context" "fmt" - "os" - "path/filepath" "testing" "github.com/open-policy-agent/conftest/internal/network" @@ -78,28 +76,3 @@ func TestSetupClient(t *testing.T) { }) } } - -func TestSetupClientCredentialsError(t *testing.T) { - tmpDir := t.TempDir() - configFilePath := filepath.Join(tmpDir, "config.json") - - // Write file with no permissions - err := os.WriteFile(configFilePath, []byte(`{"auths":{}}`), 0o000) - if err != nil { - t.Fatalf("failed to write to config file: %v", err) - } - - // Ensure permissions are restored for cleanup - t.Cleanup(func() { - if err := os.Chmod(configFilePath, 0o600); err != nil { - t.Errorf("failed to restore permissions during cleanup: %v", err) - } - }) - - repository := mustParseReference("local-test-registry/image:tag") - t.Setenv("DOCKER_CONFIG", configFilePath) - - if err := SetupClient(repository); err == nil { - t.Error("expected error when credentials store initialization fails, got nil") - } -} diff --git a/internal/registry/client_unix_test.go b/internal/registry/client_unix_test.go new file mode 100644 index 000000000..337633675 --- /dev/null +++ b/internal/registry/client_unix_test.go @@ -0,0 +1,26 @@ +//go:build unix + +package registry + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSetupClientCredentialsError(t *testing.T) { + configFilePath := filepath.Join(t.TempDir(), "config.json") + + // Write file with no permissions + err := os.WriteFile(configFilePath, []byte(`{"auths":{}}`), 0o000) + if err != nil { + t.Fatalf("failed to write to config file: %v", err) + } + + repository := mustParseReference("local-test-registry/image:tag") + t.Setenv("DOCKER_CONFIG", configFilePath) + + if err := SetupClient(repository); err == nil { + t.Error("expected error when credentials store initialization fails, got nil") + } +} diff --git a/plugin/install_test.go b/plugin/install_test.go index e64413999..751215d66 100644 --- a/plugin/install_test.go +++ b/plugin/install_test.go @@ -92,7 +92,7 @@ func TestInstall(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(XDGDataHome, tmpDir) + t.Setenv("XDG_DATA_HOME", tmpDir) source, wantPluginName := tt.setup(t) err := Install(t.Context(), source) diff --git a/plugin/xdg.go b/plugin/xdg.go index 6db157220..3ee42f7e0 100644 --- a/plugin/xdg.go +++ b/plugin/xdg.go @@ -4,35 +4,26 @@ import ( "fmt" "os" "path/filepath" - "strings" -) - -const ( - // XDGDataHome is the directory to search for data files in the XDG spec - XDGDataHome = "XDG_DATA_HOME" - // XDGDataDirs defines an additional list of directories which can be searched for data files - XDGDataDirs = "XDG_DATA_DIRS" + "github.com/tzrikka/xdg" ) type xdgPath string // Preferred returns the preferred path according to the XDG specification func (p xdgPath) Preferred(path string) string { - return p.preferred(path, os.Getenv(XDGDataHome), os.Getenv(XDGDataDirs)) + return p.preferred(path, xdg.MustDataHome(), xdg.MustDataDirs()) } -func (p xdgPath) preferred(path, xdgDataHome, xdgDataDirs string) string { +func (p xdgPath) preferred(path, xdgDataHome string, xdgDataDirs []string) string { if xdgDataHome != "" && p.writable(xdgDataHome) { return filepath.ToSlash(filepath.Join(xdgDataHome, string(p), path)) } - if xdgDataDirs != "" { - // Pick the first dir that is writable. - for dir := range strings.SplitSeq(xdgDataDirs, ":") { - if p.writable(dir) { - return filepath.ToSlash(filepath.Join(dir, string(p), path)) - } + // Pick the first dir that is writable. + for _, dir := range xdgDataDirs { + if p.writable(dir) { + return filepath.ToSlash(filepath.Join(dir, string(p), path)) } } @@ -59,10 +50,10 @@ func (p xdgPath) writable(path string) bool { // preference order. If no error is returned, the given string indicates // where the file was found. func (p xdgPath) Find(path string) (string, error) { - return p.find(path, os.Getenv(XDGDataHome), os.Getenv(XDGDataDirs)) + return p.find(path, xdg.MustDataHome(), xdg.MustDataDirs()) } -func (p xdgPath) find(path, xdgDataHome, xdgDataDirs string) (string, error) { +func (p xdgPath) find(path, xdgDataHome string, xdgDataDirs []string) (string, error) { if xdgDataHome != "" { dir := filepath.ToSlash(filepath.Join(xdgDataHome, string(p), path)) _, err := os.Stat(dir) @@ -74,16 +65,14 @@ func (p xdgPath) find(path, xdgDataHome, xdgDataDirs string) (string, error) { } } - if xdgDataDirs != "" { - for dataDir := range strings.SplitSeq(xdgDataDirs, ":") { - dir := filepath.ToSlash(filepath.Join(dataDir, string(p), path)) - _, err := os.Stat(dir) - if err != nil && !os.IsNotExist(err) { - return "", fmt.Errorf("get data dirs directory: %w", err) - } - if err == nil { - return dir, nil - } + for _, dataDir := range xdgDataDirs { + dir := filepath.ToSlash(filepath.Join(dataDir, string(p), path)) + _, err := os.Stat(dir) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("get data dirs directory: %w", err) + } + if err == nil { + return dir, nil } } diff --git a/plugin/xdg_test.go b/plugin/xdg_test.go index 4395567f9..f30104a91 100644 --- a/plugin/xdg_test.go +++ b/plugin/xdg_test.go @@ -25,7 +25,7 @@ func TestPreferred(t *testing.T) { name string path string xdgDataHome string - xdgDataDirs string + xdgDataDirs []string want string }{ { @@ -43,13 +43,13 @@ func TestPreferred(t *testing.T) { name: "should return XDG_DATA_HOME if both XDG_DATA_HOME and XDG_DATA_DIRS is set", path: pluginsDir, xdgDataHome: tempDir, - xdgDataDirs: "/tmp2:/tmp3", + xdgDataDirs: []string{"/tmp2", "/tmp3"}, want: filepath.Join(tempDir, conftestDir, pluginsDir), }, { name: "should return first XDG_DATA_DIRS that exists if only XDG_DATA_DIRS is set", path: pluginsDir, - xdgDataDirs: nonExistant + ":" + tempDir, + xdgDataDirs: []string{nonExistant, tempDir}, want: filepath.Join(tempDir, conftestDir, pluginsDir), }, } @@ -57,9 +57,11 @@ func TestPreferred(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() + want := filepath.ToSlash(tt.want) + xdg := xdgPath(conftestDir) - if got := xdg.preferred(tt.path, tt.xdgDataHome, tt.xdgDataDirs); got != tt.want { - t.Errorf("xdgPath.Preferred() = %v, want %v", got, tt.want) + if got := xdg.preferred(tt.path, tt.xdgDataHome, tt.xdgDataDirs); got != want { + t.Errorf("xdgPath.Preferred() = %v, want %v", got, want) } }) } @@ -93,7 +95,7 @@ func TestFind(t *testing.T) { name string path string xdgDataHome string - xdgDataDirs string + xdgDataDirs []string want string }{ { @@ -103,32 +105,34 @@ func TestFind(t *testing.T) { { name: "should use homeDir if no XDG path is set.", path: pluginsDir, - want: filepath.ToSlash(filepath.Join(userHome, cacheDir, pluginsDir)), + want: filepath.Join(userHome, cacheDir, pluginsDir), }, { name: "should use XDG_DATA_HOME if set", path: pluginsDir, xdgDataHome: tempDir, - want: filepath.ToSlash(filepath.Join(tempDir, cacheDir, pluginsDir)), + want: filepath.Join(tempDir, cacheDir, pluginsDir), }, { name: "should use first existing XDG_DATA_DIRS if set", path: pluginsDir, - xdgDataDirs: "/does/not/exist:" + tempDir, - want: filepath.ToSlash(filepath.Join(tempDir, cacheDir, pluginsDir)), + xdgDataDirs: []string{nonExistant, tempDir}, + want: filepath.Join(tempDir, cacheDir, pluginsDir), }, { name: "fall back to homeDir if XDG dirs point at non-existent paths", path: pluginsDir, xdgDataHome: nonExistant, - xdgDataDirs: nonExistant, - want: filepath.ToSlash(filepath.Join(userHome, cacheDir, pluginsDir)), + xdgDataDirs: []string{nonExistant}, + want: filepath.Join(userHome, cacheDir, pluginsDir), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + want := filepath.ToSlash(tt.want) + xdg := xdgPath(cacheDir) got, err := xdg.find(tt.path, tt.xdgDataHome, tt.xdgDataDirs) gotErr := err != nil @@ -136,8 +140,8 @@ func TestFind(t *testing.T) { if gotErr != wantErr { t.Fatalf("xdgPath.Find() error = %v, wantErr %v", err, wantErr) } - if got != tt.want { - t.Errorf("xdgPath.Find() = %s, want %s", got, tt.want) + if got != want { + t.Errorf("xdgPath.Find() = %s, want %s", got, want) } }) }