Skip to content

Commit

Permalink
Add RE2 regex support for module updates
Browse files Browse the repository at this point in the history
Add new option `--source-match-type` to define the type of the match.
Possible values are `full` and `regex`. The default value is `full`.

The match type `full` is the same as the current behavior.

The match type `regex` is the new behavior. It uses RE2 regex to match
the module source URL with the given name pattern.

```bash
tfupdate module -v 2.14.1 --source-match-type regex terraform-aws-modules/s3-bucket main.tf
```
  • Loading branch information
fr12k committed Jan 19, 2024
1 parent 69e4241 commit 94700d7
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 109 deletions.
2 changes: 1 addition & 1 deletion command/lock.go
Expand Up @@ -53,7 +53,7 @@ func (c *LockCommand) Run(args []string) int {
}

log.Println("[INFO] Update dependency lock files")
option, err := tfupdate.NewOption("lock", "", "", c.platforms, c.recursive, c.ignorePaths)
option, err := tfupdate.NewOption("lock", "", "", c.platforms, c.recursive, c.ignorePaths, "")
if err != nil {
c.UI.Error(err.Error())
return 1
Expand Down
28 changes: 16 additions & 12 deletions command/module.go
Expand Up @@ -13,11 +13,12 @@ import (
// ModuleCommand is a command which update version constraints for module.
type ModuleCommand struct {
Meta
name string
version string
path string
recursive bool
ignorePaths []string
name string
version string
path string
recursive bool
ignorePaths []string
sourceMatchType string
}

// Run runs the procedure of this command.
Expand All @@ -26,6 +27,7 @@ func (c *ModuleCommand) Run(args []string) int {
cmdFlags.StringVarP(&c.version, "version", "v", "", "A new version constraint")
cmdFlags.BoolVarP(&c.recursive, "recursive", "r", false, "Check a directory recursively")
cmdFlags.StringArrayVarP(&c.ignorePaths, "ignore-path", "i", []string{}, "A regular expression for path to ignore")
cmdFlags.StringVar(&c.sourceMatchType, "source-match-type", "full", "Define how to match module source URLs. Valid values are \"full\" or \"regex\".")

if err := cmdFlags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err))
Expand All @@ -50,7 +52,7 @@ func (c *ModuleCommand) Run(args []string) int {
}

log.Printf("[INFO] Update module %s to %s", c.name, v)
option, err := tfupdate.NewOption("module", c.name, v, []string{}, c.recursive, c.ignorePaths)
option, err := tfupdate.NewOption("module", c.name, v, []string{}, c.recursive, c.ignorePaths, c.sourceMatchType)
if err != nil {
c.UI.Error(err.Error())
return 1
Expand All @@ -77,18 +79,20 @@ func (c *ModuleCommand) Help() string {
Usage: tfupdate module [options] <MODULE_NAME> <PATH>
Arguments
MODULE_NAME A name of module
MODULE_NAME A name of module or a regular expression in RE2 syntax
e.g.
terraform-aws-modules/vpc/aws
git::https://example.com/vpc.git
git::https://example.com/.+
PATH A path of file or directory to update
Options:
-v --version A new version constraint (required)
Automatic latest version resolution is not currently supported for modules.
-r --recursive Check a directory recursively (default: false)
-i --ignore-path A regular expression for path to ignore
If you want to ignore multiple directories, set the flag multiple times.
-v --version A new version constraint (required)
Automatic latest version resolution is not currently supported for modules.
-r --recursive Check a directory recursively (default: false)
-i --ignore-path A regular expression for path to ignore
If you want to ignore multiple directories, set the flag multiple times.
--source-match-type Define how to match MODULE_NAME to the module source URLs. Valid values are "full" or "regex". (default: full)
`
return strings.TrimSpace(helpText)
}
Expand Down
2 changes: 1 addition & 1 deletion command/provider.go
Expand Up @@ -65,7 +65,7 @@ func (c *ProviderCommand) Run(args []string) int {
}

log.Printf("[INFO] Update provider %s to %s", c.name, v)
option, err := tfupdate.NewOption("provider", c.name, v, []string{}, c.recursive, c.ignorePaths)
option, err := tfupdate.NewOption("provider", c.name, v, []string{}, c.recursive, c.ignorePaths, "")
if err != nil {
c.UI.Error(err.Error())
return 1
Expand Down
10 changes: 5 additions & 5 deletions command/terraform.go
Expand Up @@ -14,10 +14,10 @@ import (
// TerraformCommand is a command which update version constraints for terraform.
type TerraformCommand struct {
Meta
version string
path string
recursive bool
ignorePaths []string
version string
path string
recursive bool
ignorePaths []string
}

// Run runs the procedure of this command.
Expand Down Expand Up @@ -56,7 +56,7 @@ func (c *TerraformCommand) Run(args []string) int {
}

log.Printf("[INFO] Update terraform to %s", v)
option, err := tfupdate.NewOption("terraform", "", v, []string{}, c.recursive, c.ignorePaths)
option, err := tfupdate.NewOption("terraform", "", v, []string{}, c.recursive, c.ignorePaths, "")
if err != nil {
c.UI.Error(err.Error())
return 1
Expand Down
26 changes: 20 additions & 6 deletions tfupdate/module.go
Expand Up @@ -20,12 +20,13 @@ var moduleSourceRegexp = regexp.MustCompile(`(.+)\?ref=v([0-9]+(\.[0-9]+)*(-.*)*

// ModuleUpdater is a updater implementation which updates the module version constraint.
type ModuleUpdater struct {
name string
version string
name string
sourceMatchType string
version string
}

// NewModuleUpdater is a factory method which returns an ModuleUpdater instance.
func NewModuleUpdater(name string, version string) (Updater, error) {
func NewModuleUpdater(name string, version string, sourceMatchType string) (Updater, error) {
if len(name) == 0 {
return nil, errors.Errorf("failed to new module updater. name is required")
}
Expand All @@ -35,8 +36,9 @@ func NewModuleUpdater(name string, version string) (Updater, error) {
}

return &ModuleUpdater{
name: name,
version: version,
name: name,
sourceMatchType: sourceMatchType,
version: version,
}, nil
}

Expand All @@ -51,12 +53,24 @@ func (u *ModuleUpdater) Update(_ context.Context, _ *ModuleContext, filename str
return u.updateModuleBlock(f)
}

func match(match, matchType, name string) bool {
switch matchType {
case "regex":
var re = regexp.MustCompile(match)
return len(re.FindAllString(name, 1)) > 0
case "full":
fallthrough
default:
return match == name
}
}

func (u *ModuleUpdater) updateModuleBlock(f *hclwrite.File) error {
for _, m := range allMatchingBlocksByType(f.Body(), "module") {
if s := m.Body().GetAttribute("source"); s != nil {
name, version := parseModuleSource(s)
// If this module is a target module
if name == u.name {
if match(u.name, u.sourceMatchType, name) {
if len(version) == 0 {
// The source attribute doesn't have a version number.
// Set a version to attribute value only if the version key exists.
Expand Down
136 changes: 101 additions & 35 deletions tfupdate/module_test.go
Expand Up @@ -11,36 +11,41 @@ import (

func TestNewModuleUpdater(t *testing.T) {
cases := []struct {
name string
version string
want Updater
ok bool
name string
sourceMatchType string
version string
want Updater
ok bool
}{
{
name: "terraform-aws-modules/vpc/aws",
version: "2.17.0",
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "2.17.0",
want: &ModuleUpdater{
name: "terraform-aws-modules/vpc/aws",
version: "2.17.0",
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "2.17.0",
},
ok: true,
},
{
name: "",
version: "2.17.0",
want: nil,
ok: false,
name: "",
sourceMatchType: "full",
version: "2.17.0",
want: nil,
ok: false,
},
{
name: "terraform-aws-modules/vpc/aws",
version: "",
want: nil,
ok: false,
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "",
want: nil,
ok: false,
},
}

for _, tc := range cases {
got, err := NewModuleUpdater(tc.name, tc.version)
got, err := NewModuleUpdater(tc.name, tc.version, tc.sourceMatchType)
if tc.ok && err != nil {
t.Errorf("NewModuleUpdater() with name = %s, version = %s returns unexpected err: %+v", tc.name, tc.version, err)
}
Expand All @@ -57,12 +62,13 @@ func TestNewModuleUpdater(t *testing.T) {

func TestUpdateModule(t *testing.T) {
cases := []struct {
filename string
src string
name string
version string
want string
ok bool
filename string
src string
name string
sourceMatchType string
version string
want string
ok bool
}{
{
filename: "main.tf",
Expand All @@ -72,8 +78,9 @@ module "vpc" {
version = "2.17.0"
}
`,
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
sourceMatchType: "full",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
Expand All @@ -94,8 +101,9 @@ module "vpc2" {
version = "2.17.0"
}
`,
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
sourceMatchType: "full",
want: `
module "vpc1" {
source = "terraform-aws-modules/vpc/aws"
Expand All @@ -116,8 +124,9 @@ module "vpc" {
version = "2.17.0"
}
`,
name: "terraform-aws-modules/hoge/aws",
version: "2.18.0",
name: "terraform-aws-modules/hoge/aws",
sourceMatchType: "full",
version: "2.18.0",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
Expand All @@ -133,8 +142,9 @@ module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}
`,
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "2.18.0",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
Expand All @@ -149,21 +159,77 @@ module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
`,
name: "git::https://example.com/vpc.git",
version: "1.3.0",
name: "git::https://example.com/vpc.git",
sourceMatchType: "full",
version: "1.3.0",
want: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.3.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.17.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.17.0"
}
`,
name: "terraform-aws-modules.git/",
version: "2.18.0",
sourceMatchType: "regex",
want: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.18.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.18.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.17.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.17.0"
}
`,
name: "terraform-aws-modules\\.git/.+",
version: "2.18.0",
sourceMatchType: "regex",
want: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.18.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.18.0"
}
`,
ok: true,
},
}

for _, tc := range cases {
u := &ModuleUpdater{
name: tc.name,
version: tc.version,
name: tc.name,
sourceMatchType: tc.sourceMatchType,
version: tc.version,
}
f, diags := hclwrite.ParseConfig([]byte(tc.src), tc.filename, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
Expand Down

0 comments on commit 94700d7

Please sign in to comment.