diff --git a/README.md b/README.md index 888675c..d1524b7 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,35 @@ Navigate to the following examples to see practical usage of `mod`: ## API Reference +## `regexpReplace` provisioner + +`regexpReplace` updates any text file like Dockerfile with regular expressions. + +Let's say you want to automate updating the base image of the below Dockerfile: + +``` +FROM helmfile:0.94.0 + +RUN echo hello +``` + +You can write a `variant.mod` file like the below so that `mod` knows where is the image tag to be updated: + +```yaml +provisioners: + regexpReplace: + Dockerfile: + from: "(FROM helmfile:)(\\S+)(\\s+)" + to: "${1}{{.Dependencies.helmfile.version}}${3}" + +dependencies: + helmfile: + releasesFrom: + dockerImageTags: + source: quay.io/roboll/helmfile + version: "> 0.94.0" +``` + ### `docker` executable provisioner Setting `provisioners.executables.NAME.platforms[].docker` allows you to run `mod exec -- NAME $args` where the executable is backed by a docker image which is managed by `mod`. diff --git a/pkg/variantmod/manager.go b/pkg/variantmod/manager.go index 961d2df..c92f159 100644 --- a/pkg/variantmod/manager.go +++ b/pkg/variantmod/manager.go @@ -22,6 +22,7 @@ import ( "k8s.io/klog/klogr" "os" "path/filepath" + "regexp" "strings" ) @@ -45,10 +46,11 @@ type ParametersSpec struct { } type ProvisionersSpec struct { - Files map[string]FileSpec `yaml:"files"` - Executables execversionmanager.Config `yaml:",inline"` - TextReplace map[string]TextReplaceSpec `yaml:"textReplace"` - YamlPatch map[string][]YamlPatchSpec `yaml:"yamlPatch"` + Files map[string]FileSpec `yaml:"files"` + Executables execversionmanager.Config `yaml:",inline"` + TextReplace map[string]TextReplaceSpec `yaml:"textReplace"` + RegexpReplace map[string]RegexpReplaceSpec `yaml:"regexpReplace"` + YamlPatch map[string][]YamlPatchSpec `yaml:"yamlPatch"` } type FileSpec struct { @@ -61,6 +63,11 @@ type TextReplaceSpec struct { To string `yaml:"to"` } +type RegexpReplaceSpec struct { + From string `yaml:"from"` + To string `yaml:"to"` +} + type YamlPatchSpec struct { Op string `yaml:"op"` Path string `yaml:"path"` @@ -492,6 +499,16 @@ func (m *ModuleManager) load(depspec DependencySpec) (mod *Module, err error) { files = append(files, f) } + regexpReplaces := []RegexpReplace{} + for path, tspec := range spec.Provisioners.RegexpReplace { + t := RegexpReplace{ + Path: path, + From: tspec.From, + To: tspec.To, + } + regexpReplaces = append(regexpReplaces, t) + } + textReplaces := []TextReplace{} for path, tspec := range spec.Provisioners.TextReplace { t := TextReplace{ @@ -554,6 +571,7 @@ func (m *ModuleManager) load(depspec DependencySpec) (mod *Module, err error) { Values: vals, ValuesSchema: schema, Files: files, + RegexpReplaces: regexpReplaces, TextReplaces: textReplaces, Yamls: yamls, Executable: execset, @@ -670,7 +688,7 @@ func (m *ModuleManager) PullRequest(title, body, base, head string, skipDuplicat owner := ownerRepo[0] repo := ownerRepo[1] - b, err:= tmpl.Render("body", body, mod.Values) + b, err := tmpl.Render("body", body, mod.Values) if err != nil { return err } @@ -997,6 +1015,49 @@ func (m *ModuleManager) doBuildSingle(mod *Module) (r *BuildResult, err error) { r.Files = append(r.Files, t.Path) } + for _, t := range mod.RegexpReplaces { + from, err := regexp.Compile(t.From) + if err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + to, err := tmpl.Render("to", t.To, values) + if err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + to = strings.TrimSpace(to) + + path, err := tmpl.Render("path", t.Path, values) + if err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + target := filepath.Join(m.AbsWorkDir, path) + m.Logger.V(1).Info("regexpReplace", "path", target, "from", from, "to", to) + contents, err := m.fs.ReadFile(target) + if err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + res, err := regexpReplace(contents, from, to) + if err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + if err := m.fs.WriteFile(target, res, 0644); err != nil { + m.Logger.V(1).Info(err.Error()) + return nil, err + } + + r.Files = append(r.Files, t.Path) + } + for _, y := range mod.Yamls { path, err := tmpl.Render("path", y.Path, values) if err != nil { diff --git a/pkg/variantmod/manager_test.go b/pkg/variantmod/manager_test.go index da2c306..26c0095 100644 --- a/pkg/variantmod/manager_test.go +++ b/pkg/variantmod/manager_test.go @@ -815,3 +815,126 @@ nodeGroups: } } + + +func TestDependencyLockinge_Dockerfile_RegexpReplace(t *testing.T) { + if testing.Verbose() { + } + + files := map[string]interface{}{ + "/path/to/variant.mod": ` +name: myapp + +provisioners: + regexpReplace: + Dockerfile: + from: "(FROM helmfile:)(\\S+)(\\s+)" + to: "${1}{{.Dependencies.helmfile.version}}${3}" + +dependencies: + helmfile: + releasesFrom: + exec: + command: go + args: + - run + - main.go + version: "> 0.94.0" +`, + "/path/to/Dockerfile": `FROM helmfile:0.94.0 + +RUN echo hello +`, + "/path/to/variant.lock": ` +dependencies: + helmfile: + version: "0.94.1" +`, + } + fs, clean, err := vfst.NewTestFS(files) + if err != nil { + t.Fatal(err) + } + defer clean() + log := klogr.New() + klog.SetOutput(os.Stderr) + klog.V(2).Info(fmt.Sprintf("temp dir: %v", fs.TempDir())) + + expectedInput := cmdsite.NewInput("go", []string{"run", "main.go"}, map[string]string{}) + expectedStdout := `0.94.1 +0.95.0 +` + cmdr := cmdsite.NewTester(map[cmdsite.CommandInput]cmdsite.CommandOutput{ + expectedInput: {Stdout: expectedStdout}, + }) + + man, err := New(Logger(log), FS(fs), WD("/path/to"), GoGetterWD(filepath.Join(fs.TempDir(), "path", "to")), Commander(cmdr)) + if err != nil { + t.Fatal(err) + } + + mod, err := man.Load() + if err != nil { + t.Fatal(err) + } + + if _, err := man.doBuild(mod); err != nil { + t.Fatal(err) + } + + dockerfile1Expected := `FROM helmfile:0.94.1 + +RUN echo hello +` + dockerfile1Actual, err := fs.ReadFile("/path/to/Dockerfile") + if err != nil { + t.Fatal(err) + } + if string(dockerfile1Actual) != dockerfile1Expected { + t.Errorf("assertion failed: expected=%s, got=%s", dockerfile1Expected, string(dockerfile1Actual)) + } + + upMod, err := man.doUp() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := man.lock(upMod); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + lockActual, err := fs.ReadFile("/path/to/variant.lock") + if err != nil { + t.Fatal(err) + } + lockExpected := `dependencies: + helmfile: + version: 0.95.0 + previousVersion: 0.94.1 +` + if string(lockActual) != lockExpected { + t.Errorf("assertion failed: expected=%s, got=%s", lockExpected, string(lockActual)) + } + + mod2, err := man.Load() + if err != nil { + t.Fatal(err) + } + + if _, err := man.doBuild(mod2); err != nil { + t.Fatal(err) + } + + dockerfile2Expected := `FROM helmfile:0.95.0 + +RUN echo hello +` + dockerfile2Actual, err := fs.ReadFile("/path/to/Dockerfile") + if err != nil { + t.Fatal(err) + } + if string(dockerfile2Actual) != dockerfile2Expected { + t.Errorf("assertion failed: expected=%s, got=%s", dockerfile2Expected, string(dockerfile2Actual)) + } + +} diff --git a/pkg/variantmod/module.go b/pkg/variantmod/module.go index 58379cc..9022142 100644 --- a/pkg/variantmod/module.go +++ b/pkg/variantmod/module.go @@ -13,11 +13,12 @@ type Values map[string]interface{} type Module struct { Alias string - Values Values - ValuesSchema Values - Files []File - TextReplaces []TextReplace - Yamls []YamlPatch + Values Values + ValuesSchema Values + Files []File + TextReplaces []TextReplace + RegexpReplaces []RegexpReplace + Yamls []YamlPatch ReleaseChannel *releasetracker.Tracker Executable *execversionmanager.ExecVM @@ -30,7 +31,7 @@ type Module struct { type ModVersionLock struct { Dependencies map[string]DepVersionLock `yaml:"dependencies"` - RawLock string `yaml:"-"` + RawLock string `yaml:"-"` } type DepVersionLock struct { @@ -65,6 +66,11 @@ type TextReplace struct { From, To string } +type RegexpReplace struct { + Path string + From, To string +} + type YamlPatch struct { Path string Patches []Patch diff --git a/pkg/variantmod/regexp_replace.go b/pkg/variantmod/regexp_replace.go new file mode 100644 index 0000000..32994b0 --- /dev/null +++ b/pkg/variantmod/regexp_replace.go @@ -0,0 +1,19 @@ +package variantmod + +import ( + "regexp" +) + +func regexpReplace(source []byte, pat *regexp.Regexp, template string) ([]byte, error) { + var ( + cur int + res []byte + ) + for _, m := range pat.FindAllSubmatchIndex(source, -1) { + res = append(res, source[cur:m[0]]...) + res = pat.Expand(res, []byte(template), source, m) + cur = m[1] + } + res = append(res, source[cur:]...) + return res, nil +}