Skip to content

Commit

Permalink
yaml/v2: Detect file patterns automatically (#2873)
Browse files Browse the repository at this point in the history
<!--Thanks for your contribution. See [CONTRIBUTING](CONTRIBUTING.md)
    for Pulumi's contribution guidelines.

    Help us merge your changes more quickly by adding more details such
    as labels, milestones, and reviewers.-->

### Proposed changes

<!--Give us a brief description of what you've done and what it solves.
-->

This PR teaches `ConfigGroup` to detect whether a given file path is a
glob pattern, to make the handling of non-existent files be consistent
w.r.t `ConfigFile`. In other words, `*.yaml` MAY match a file whereas
`manifest.yaml` MUST match a file.

The detection code looks for special characters '*', '?', and '[' and
respects the escape syntax. Intended to be consistent with:
[https://pkg.go.dev/path/filepath#Match](https://pkg.go.dev/path/filepath#Match)

An alternative to doing pattern detection would be:
1. to expose a `glob` property to toggle globbing, or 
2. a `patterns` property alongside the `files` property for patterns and
non-patterns respectively

### Related issues (optional)

<!--Refer to related PRs or issues: #1234, or 'Fixes #1234' or 'Closes
#1234'.
Or link to full URLs to issues or pull requests in other GitHub
repositories. -->
Closes #2871
  • Loading branch information
EronWright authored Mar 12, 2024
1 parent 101fcfb commit 9595b1a
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 10 deletions.
15 changes: 12 additions & 3 deletions provider/pkg/provider/yaml/v2/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -70,10 +71,10 @@ func ParseDecodeYamlFiles(ctx *pulumi.Context, args *ParseArgs, glob bool, clien
}
yamls = append(yamls, string(yaml))
} else {
// Otherwise, assume this is a path to a file on disk. If globbing is enabled, we might have
// multiple files -- otherwise just read a singular file.
// Otherwise, assume this is a path to a file on disk. If globbing is enabled and a pattern is provided, we might have
// multiple files -- otherwise just read a singular file and fail if it doesn't exist.
var files []string
if glob {
if glob && isGlobPattern(file) {
files, err = filepath.Glob(file)
if err != nil {
return pulumi.ArrayOutput{}, errors.Wrapf(err, "expanding glob")
Expand Down Expand Up @@ -246,3 +247,11 @@ func printUnstructured(obj *unstructured.Unstructured) string {
bytes, _ := obj.MarshalJSON()
return truncate(strings.TrimSpace(string(bytes)), 100)
}

// globPatternRegexp is a regular expression that matches any of the special characters in a glob pattern.
// see: https://pkg.go.dev/path/filepath#Match
var globPatternRegexp = regexp.MustCompile(`(?:^|[^\\])[*?\[]`)

func isGlobPattern(pattern string) bool {
return globPatternRegexp.Match([]byte(pattern))
}
61 changes: 54 additions & 7 deletions provider/pkg/provider/yaml/v2/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"os"
"path/filepath"
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand All @@ -26,6 +27,7 @@ import (
. "github.com/pulumi/pulumi-kubernetes/tests/v4/gomega"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/internals"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

Expand Down Expand Up @@ -195,20 +197,34 @@ var _ = Describe("ParseDecodeYamlFiles", func() {
})

Describe("files", func() {
Context("when the file doesn't exist (glob mode)", func() {
Describe("globbing", func() {
BeforeEach(func() {
glob = true
args.Files = []string{"nosuchfile.yaml"}
})
It("should do nothing", func(ctx context.Context) {
_, err := parse(ctx)
Expect(err).ShouldNot(HaveOccurred())

Context("when the pattern matches no files", func() {
BeforeEach(func() {
args.Files = []string{"nosuchfile-*.yaml"}
})
It("should do nothing", func(ctx context.Context) {
_, err := parse(ctx)
Expect(err).ShouldNot(HaveOccurred())
})
})

Context("when the pattern matches some files", func() {
BeforeEach(func() {
tempDir := GinkgoTB().TempDir()
err := os.WriteFile(filepath.Join(tempDir, "manifest.yaml"), []byte(manifest), 0o600)
Expect(err).ShouldNot(HaveOccurred())
args.Files = []string{filepath.Join(tempDir, "*.yaml")}
})
commonAssertions()
})
})

Context("when the file doesn't exist (non-glob mode)", func() {
Context("when the file doesn't exist", func() {
BeforeEach(func() {
glob = false
args.Files = []string{"nosuchfile.yaml"}
})
It("should fail", func(ctx context.Context) {
Expand Down Expand Up @@ -330,3 +346,34 @@ var _ = Describe("ParseDecodeYamlFiles", func() {
})
})
})

func TestIsGlobPattern(t *testing.T) {
t.Parallel()

tests := []struct {
pattern string
expected bool
}{
{pattern: `manifest.yaml`, expected: false},
{pattern: `*.yaml`, expected: true},
{pattern: `*`, expected: true},
{pattern: `test-?.yaml`, expected: true},
{pattern: `ba[rz].yaml`, expected: true},
{pattern: `escaped-\*.yaml`, expected: false},
{pattern: `\*.yaml`, expected: false},
}

for _, tt := range tests {
tt := tt
t.Run(tt.pattern, func(t *testing.T) {
t.Parallel()

isPattern := isGlobPattern(tt.pattern)
if tt.expected {
assert.Truef(t, isPattern, "expected %q to be a pattern", tt.pattern)
} else {
assert.Falsef(t, isPattern, "expected %q to not be a pattern", tt.pattern)
}
})
}
}

0 comments on commit 9595b1a

Please sign in to comment.