Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ spec:
| replacements | [][EventWatcherReplacement](/docs/user-guide/configuration-reference/#eventwatcherreplacement) | List of places where will be replaced when the new event matches. | Yes |

## EventWatcherReplacement
One of `yamlField` or `regex` is required.

| Field | Type | Description | Required |
|-|-|-|-|
| file | string | The path to the file to be updated. | Yes |
| yamlField | string | The yaml path to the field to be updated. It requires to start with `$` which represents the root element. e.g. `$.foo.bar[0].baz`. | Yes |
| yamlField | string | The yaml path to the field to be updated. It requires to start with `$` which represents the root element. e.g. `$.foo.bar[0].baz`. | No |
| regex | string | The regex string that specify what should be replaced. The only first capturing group enclosed by `()` will be replaced with the new value. e.g. `host.xz/foo/bar:(v[0-9].[0-9].[0-9])` | No |

## CommitMatcher

Expand Down
1 change: 1 addition & 0 deletions pkg/app/piped/eventwatcher/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"//pkg/config:go_default_library",
"//pkg/git:go_default_library",
"//pkg/model:go_default_library",
"//pkg/regexpool:go_default_library",
"//pkg/yamlprocessor:go_default_library",
"@org_uber_go_zap//:go_default_library",
],
Expand Down
58 changes: 57 additions & 1 deletion pkg/app/piped/eventwatcher/eventwatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp/syntax"
"strconv"
"sync"
"time"
Expand All @@ -33,6 +34,7 @@ import (
"github.com/pipe-cd/pipe/pkg/config"
"github.com/pipe-cd/pipe/pkg/git"
"github.com/pipe-cd/pipe/pkg/model"
"github.com/pipe-cd/pipe/pkg/regexpool"
"github.com/pipe-cd/pipe/pkg/yamlprocessor"
)

Expand Down Expand Up @@ -239,6 +241,8 @@ func (w *watcher) commitFiles(ctx context.Context, eventCfg config.EventWatcherE
// TODO: Empower Event watcher to parse JSON format
case r.HCLField != "":
// TODO: Empower Event watcher to parse HCL format
case r.Regex != "":
newContent, upToDate, err = modifyText(path, r.Regex, latestEvent.Data)
}
if err != nil {
return err
Expand Down Expand Up @@ -269,7 +273,7 @@ func (w *watcher) commitFiles(ctx context.Context, eventCfg config.EventWatcherE
// modifyYAML returns a new YAML content as a first returned value if the value of given
// field was outdated. True as a second returned value means it's already up-to-date.
func modifyYAML(path, field, newValue string) ([]byte, bool, error) {
yml, err := ioutil.ReadFile(path)
yml, err := os.ReadFile(path)
if err != nil {
return nil, false, fmt.Errorf("failed to read file: %w", err)
}
Expand Down Expand Up @@ -320,3 +324,55 @@ func convertStr(value interface{}) (out string, err error) {
}
return
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits, should we add a comment for this just as modifyYAML function has? It's a nits and it's up to you 🙆‍♀️

// modifyText returns a new text replacing all matches of the given regex with the newValue.
// The only first capturing group enclosed by `()` will be replaced.
// True as a second returned value means it's already up-to-date.
func modifyText(path, regexText, newValue string) ([]byte, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment to show the meaning of each returned value?
Or using named return for better understand.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's more helpful 👍

content, err := os.ReadFile(path)
if err != nil {
return nil, false, fmt.Errorf("failed to read file: %w", err)
}

pool := regexpool.DefaultPool()
regex, err := pool.Get(regexText)
if err != nil {
return nil, false, fmt.Errorf("failed to compile regex text (%s): %w", regexText, err)
}

// Extract the first capturing group.
firstGroup := ""
re, err := syntax.Parse(regexText, syntax.Perl)
if err != nil {
return nil, false, fmt.Errorf("failed to parse the first capturing group regex: %w", err)
}
for _, s := range re.Sub {
if s.Op == syntax.OpCapture {
firstGroup = s.String()
break
}
}
if firstGroup == "" {
return nil, false, fmt.Errorf("capturing group not found in the given regex")
}
subRegex, err := pool.Get(firstGroup)
if err != nil {
return nil, false, fmt.Errorf("failed to compile the first capturing group: %w", err)
}

var touched, outDated bool
newText := regex.ReplaceAllFunc(content, func(match []byte) []byte {
touched = true
outDated = string(subRegex.Find(match)) != newValue
// Return text replacing the only first capturing group with the newValue.
return subRegex.ReplaceAll(match, []byte(newValue))
})
if !touched {
return nil, false, fmt.Errorf("the content of %s doesn't match %s", path, regexText)
}
if !outDated {
return nil, true, nil
}

return newText, false, nil
}
119 changes: 119 additions & 0 deletions pkg/app/piped/eventwatcher/eventwatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,122 @@ func TestModifyYAML(t *testing.T) {
})
}
}

func TestModifyText(t *testing.T) {
testcases := []struct {
name string
path string
regex string
newValue string
want []byte
wantUpToDate bool
wantErr bool
}{
{
name: "invalid regex given",
path: "testdata/with-template.yaml",
regex: "[",
newValue: "v0.2.0",
want: nil,
wantUpToDate: false,
wantErr: true,
},
{
name: "no capturing group given",
path: "testdata/with-template.yaml",
regex: "image: gcr.io/pipecd/foo:v[0-9].[0-9].[0-9]",
newValue: "v0.2.0",
want: nil,
wantUpToDate: false,
wantErr: true,
},
{
name: "invalid capturing group given",
path: "testdata/with-template.yaml",
regex: "image: gcr.io/pipecd/foo:([)",
newValue: "v0.2.0",
want: nil,
wantUpToDate: false,
wantErr: true,
},
{
name: "the file doesn't match regex",
path: "testdata/with-template.yaml",
regex: "abcdefg",
newValue: "v0.1.0",
want: nil,
wantUpToDate: false,
wantErr: true,
},
{
name: "the file is up-to-date",
path: "testdata/with-template.yaml",
regex: "image: gcr.io/pipecd/foo:(v[0-9].[0-9].[0-9])",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a case that captures/replaces the whole tag url?

newValue: "v0.1.0",
want: nil,
wantUpToDate: true,
wantErr: false,
},
{
name: "replace a part of text",
path: "testdata/with-template.yaml",
regex: "image: gcr.io/pipecd/foo:(v[0-9].[0-9].[0-9])",
newValue: "v0.2.0",
want: []byte(`apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
spec:
template:
spec:
containers:
- name: foo
image: gcr.io/pipecd/foo:v0.2.0
ports:
- containerPort: 9085
env:
- name: FOO
value: {{ .encryptedSecrets.foo }}

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
spec:
template:
spec:
containers:
- name: bar
image: gcr.io/pipecd/bar:v0.1.0
ports:
- containerPort: 9085
env:
- name: BAR
value: {{ .encryptedSecrets.bar }}
`),
wantUpToDate: false,
wantErr: false,
},
{
name: "replace text",
path: "testdata/kustomization.yaml",
regex: "newTag: (v[0-9].[0-9].[0-9])",
newValue: "v0.2.0",
want: []byte(`images:
- name: gcr.io/pipecd/foo
newTag: v0.2.0
`),
wantUpToDate: false,
wantErr: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, gotUpToDate, err := modifyText(tc.path, tc.regex, tc.newValue)
assert.Equal(t, tc.wantErr, err != nil)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.wantUpToDate, gotUpToDate)
})
}
}
3 changes: 3 additions & 0 deletions pkg/app/piped/eventwatcher/testdata/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
images:
- name: gcr.io/pipecd/foo
newTag: v0.1.0
32 changes: 32 additions & 0 deletions pkg/app/piped/eventwatcher/testdata/with-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
spec:
template:
spec:
containers:
- name: foo
image: gcr.io/pipecd/foo:v0.1.0
ports:
- containerPort: 9085
env:
- name: FOO
value: {{ .encryptedSecrets.foo }}

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
spec:
template:
spec:
containers:
- name: bar
image: gcr.io/pipecd/bar:v0.1.0
ports:
- containerPort: 9085
env:
- name: BAR
value: {{ .encryptedSecrets.bar }}
7 changes: 7 additions & 0 deletions pkg/config/event_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type EventWatcherReplacement struct {
JSONField string `json:"jsonField"`
// The HCL path to the field to be updated.
HCLField string `json:"HCLField"`
// The regex string specifying what should be replaced.
// Only the first capturing group enclosed by `()` will be replaced with the new value.
// e.g. "host.xz/foo/bar:(v[0-9].[0-9].[0-9])"
Regex string `json:"regex"`
}

// LoadEventWatcher gives back parsed EventWatcher config after merging config files placed under
Expand Down Expand Up @@ -172,6 +176,9 @@ func (e *EventWatcherEvent) Validate() error {
if r.HCLField != "" {
count++
}
if r.Regex != "" {
count++
}
if count == 0 {
return fmt.Errorf("event %q has a replacement with no field", e.Name)
}
Expand Down