diff --git a/go.mod b/go.mod index 419350a..75197c2 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.9.0 - golang.org/x/mod v0.8.0 + golang.org/x/mod v0.19.0 ) require ( diff --git a/go.sum b/go.sum index 970598f..6e59254 100644 --- a/go.sum +++ b/go.sum @@ -920,6 +920,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/plugin.go b/plugin.go index a3362fd..08bfb8c 100644 --- a/plugin.go +++ b/plugin.go @@ -10,18 +10,22 @@ import ( "golang.org/x/mod/modfile" ) -// indirectRequires returns the indirect dependencies of the go.sum file. -func indirectRequires(goSum string) (map[string]struct{}, error) { - dir := filepath.Dir(goSum) - filename := filepath.Join(dir, "go.mod") +// goMod returns the go.mod file path from the go.sum file path. +func goMod(goSum string) string { + return filepath.Join(filepath.Dir(goSum), "go.mod") +} + +// indirectRequires returns the details and indirect dependencies of the go.sum file. +func indirectRequires(goSum string) (*modfile.File, map[string]struct{}, error) { + filename := goMod(goSum) data, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("read go.mod: %w", err) + return nil, nil, fmt.Errorf("read go.mod: %w", err) } f, err := modfile.Parse(filename, data, nil) if err != nil { - return nil, fmt.Errorf("parse go.mod: %w", err) + return nil, nil, fmt.Errorf("parse go.mod: %w", err) } indirects := map[string]struct{}{} @@ -31,7 +35,23 @@ func indirectRequires(goSum string) (map[string]struct{}, error) { } } - return indirects, nil + return f, indirects, nil +} + +// writeModFile writes the modfile.File to the go.mod file determined from goSum. +func writeModFile(goSum string, f *modfile.File) error { + f.Cleanup() + data, err := f.Format() + if err != nil { + return fmt.Errorf("format go.mod: %w", err) + } + + filename := goMod(goSum) + if err = os.WriteFile(filename, data, 0644); err != nil { + return fmt.Errorf("write go.sum: %w", err) + } + + return nil } // getBuildInfo returns the dependencies of the binary calling it. @@ -59,32 +79,72 @@ func pluginFunc(cmd *cobra.Command, _ []string) error { return nil } - if gogetEnabled { - indirects, err := indirectRequires(goSum) - if err != nil { + var indirects map[string]struct{} + var modFile *modfile.File + if gogetEnabled || fixEnabled { + if modFile, indirects, err = indirectRequires(goSum); err != nil { return err } - for _, diff := range diffs { - if diff.Name != "go" && diff.Name != "libc" { - if _, ok := indirects[diff.Name]; ok { - cmd.Printf("go mod edit --replace %s=%s@%s\n", diff.Name, diff.Name, diff.Expected) - } else { - cmd.Printf("go get %s@%s\n", diff.Name, diff.Expected) - } - continue + } + + var fixed int + for _, diff := range diffs { + if diff.Name != "go" && diff.Name != "libc" && (gogetEnabled || fixEnabled) { + if ok, err := outputOrFix(cmd, diff, modFile, indirects); err != nil { + return err + } else if ok { + fixed++ } + continue + } + + cmd.Println(diff.Name) + cmd.Println("\thave:", diff.Have) + cmd.Println("\twant:", diff.Expected) + } + + if fixed > 0 { + if err = writeModFile(goSum, modFile); err != nil { + return err + } + cmd.Printf("%d incompatibilities fixed\n", fixed) + } - cmd.Println(diff.Name) - cmd.Println("\thave:", diff.Have) - cmd.Println("\twant:", diff.Expected) + if len(diffs) != fixed { + if fixed > 0 { + return fmt.Errorf("%d incompatibilities fixed, %d remains", fixed, len(diffs)-fixed) } - } else { - for _, diff := range diffs { - cmd.Println(diff.Name) - cmd.Println("\thave:", diff.Have) - cmd.Println("\twant:", diff.Expected) + + return fmt.Errorf("%d incompatibilities found", len(diffs)) + } + + return nil +} + +// outputOrFix prints the commands to fix the incompatibility or applies the fix if fixEnabled is true. +// It returns true if the incompatibility was fixed. +func outputOrFix(cmd *cobra.Command, diff plugin.Diff, modFile *modfile.File, indirects map[string]struct{}) (bool, error) { + if _, ok := indirects[diff.Name]; ok { + if fixEnabled { + if err := modFile.AddReplace(diff.Name, "", diff.Name, diff.Expected); err != nil { + return false, fmt.Errorf("add replace: %w", err) + } + return true, nil } + + cmd.Printf("go mod edit --replace %s=%s@%s\n", diff.Name, diff.Name, diff.Expected) + + return false, nil + } + + if fixEnabled { + if err := modFile.AddRequire(diff.Name, diff.Expected); err != nil { + return false, fmt.Errorf("add require: %w", err) + } + + return true, nil } + cmd.Printf("go get %s@%s\n", diff.Name, diff.Expected) - return fmt.Errorf("%d incompatibilities found", len(diffs)) + return false, nil } diff --git a/plugin_test.go b/plugin_test.go index 9a8de01..aa0c9ef 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -2,13 +2,51 @@ package cmd import ( "bytes" + "errors" + "os" + "path/filepath" + "strings" "testing" "github.com/krakendio/krakend-cobra/v2/plugin" + "github.com/luraproject/lura/v2/core" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) +const testDir = "testdata" + +// copyDir is a helper function to copy directory entries from src to dst. +func copyDir(t *testing.T, srcSubDir, dstDir string) { + t.Helper() + + srcDir := filepath.Join(testDir, srcSubDir) + entries, err := os.ReadDir(srcDir) + if errors.Is(err, os.ErrNotExist) { + // Nothing to do. + return + } + require.NoError(t, err) + + for _, entry := range entries { + file := entry.Name() + data, err := os.ReadFile(filepath.Join(srcDir, file)) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(dstDir, file), data, 0644) + require.NoError(t, err) + } +} + +// loadFile is a helper function to load a file from the testdata directory. +func loadFile(t *testing.T, name string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join(testDir, name)) + require.NoError(t, err) + + return string(data) +} + func Test_pluginFunc(t *testing.T) { var buf bytes.Buffer cmd := &cobra.Command{} @@ -16,8 +54,8 @@ func Test_pluginFunc(t *testing.T) { localDescriber = func() plugin.Descriptor { return plugin.Descriptor{ - Go: goVersion, - Libc: libcVersion, + Go: core.GoVersion, + Libc: core.GlibcVersion, Deps: map[string]string{ "golang.org/x/mod": "v0.6.0-dev.0.20220419223038-86c51ed26bb4", "github.com/Azure/azure-sdk-for-go": "v59.3.0+incompatible", @@ -28,63 +66,105 @@ func Test_pluginFunc(t *testing.T) { defer func() { localDescriber = plugin.Local }() + goModData := loadFile(t, "go.mod") + tests := map[string]struct { - goSum string - expected string - fix bool - err string + dir string + expected string + expectedGoMod string + goVersion string + format bool + fix bool + err string }{ + "missing": { - goSum: "./testdata/missing-go.sum", - err: "open ./testdata/missing-go.sum: no such file or directory", + dir: "missing", + goVersion: goVersion, + expectedGoMod: goModData, + err: "open DIR/go.sum: no such file or directory", }, - "matching": { - goSum: "./testdata/match-go.sum", - expected: "No incompatibilities found!\n", + + "match": { + dir: "match", + goVersion: goVersion, + expectedGoMod: goModData, + expected: "No incompatibilities found!\n", }, "changes": { - goSum: "./testdata/changes-go.sum", - expected: `cloud.google.com/go - have: v0.100.3 - want: v0.100.2 -github.com/Azure/azure-sdk-for-go - have: v59.3.1+incompatible - want: v59.3.0+incompatible -golang.org/x/mod - have: v0.6.10-dev.0.20220419223038-86c51ed26bb4 - want: v0.6.0-dev.0.20220419223038-86c51ed26bb4 -`, - err: "3 incompatibilities found", + dir: "changes", + goVersion: goVersion, + expectedGoMod: goModData, + expected: loadFile(t, "changes/expected.txt"), + err: "3 incompatibilities found", }, - "fix": { - goSum: "./testdata/changes-go.sum", - fix: true, - expected: `go mod edit --replace cloud.google.com/go=cloud.google.com/go@v0.100.2 -go mod edit --replace github.com/Azure/azure-sdk-for-go=github.com/Azure/azure-sdk-for-go@v59.3.0+incompatible -go get golang.org/x/mod@v0.6.0-dev.0.20220419223038-86c51ed26bb4 + "format": { + dir: "changes", + goVersion: goVersion, + expectedGoMod: goModData, + format: true, + expected: loadFile(t, "format/expected.txt"), + err: "3 incompatibilities found", + }, + "fixed-all": { + dir: "changes", + goVersion: goVersion, + expectedGoMod: loadFile(t, "fixed-all/go.mod"), + fix: true, + expected: "3 incompatibilities fixed\n", + }, + "fixed-some": { + dir: "changes", + goVersion: "1.1.0", + expectedGoMod: loadFile(t, "fixed-some/go.mod"), + fix: true, + expected: `go + have: 1.1.0 + want: undefined +3 incompatibilities fixed `, - err: "3 incompatibilities found", + err: "3 incompatibilities fixed, 1 remains", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { buf.Reset() + // Make copies in a temporary directory so + // the original files are not modified. + tempDir := t.TempDir() orig := goSum - goSum = tc.goSum + goSum = filepath.Join(tempDir, "go.sum") + copyDir(t, tc.dir, tempDir) defer func() { goSum = orig }() - fix := gogetEnabled - gogetEnabled = tc.fix - defer func() { gogetEnabled = fix }() + // Override the global variables for the test. + format := gogetEnabled + fix := fixEnabled + gogetEnabled = tc.format + fixEnabled = tc.fix + oldGoVersion := goVersion + goVersion = tc.goVersion + defer func() { + gogetEnabled = format + fixEnabled = fix + goVersion = oldGoVersion + }() err := pluginFunc(cmd, nil) if tc.err != "" { - require.EqualError(t, err, tc.err) + require.EqualError(t, err, strings.ReplaceAll(tc.err, "DIR", tempDir)) } else { require.NoError(t, err) } require.Equal(t, tc.expected, buf.String()) + + data, err := os.ReadFile(filepath.Join(tempDir, "go.mod")) + if errors.Is(err, os.ErrNotExist) { + return + } + require.NoError(t, err) + require.Equal(t, string(tc.expectedGoMod), string(data)) }) } } diff --git a/root.go b/root.go index 6794588..5009539 100644 --- a/root.go +++ b/root.go @@ -31,6 +31,7 @@ var ( libcVersion = core.GlibcVersion checkDumpPrefix = "\t" gogetEnabled = false + fixEnabled = false DefaultRoot Root RootCommand Command @@ -110,13 +111,14 @@ func init() { goVersionFlag := StringFlagBuilder(&goVersion, "go", "g", goVersion, "The version of the go compiler used for your plugin") libcVersionFlag := StringFlagBuilder(&libcVersion, "libc", "l", "", "Version of the libc library used") gogetFlag := BoolFlagBuilder(&gogetEnabled, "format", "f", false, "Shows fix commands to update your dependencies") + fixFlag := BoolFlagBuilder(&fixEnabled, "fix", "x", false, "Applies fixes to update your dependencies") PluginCommand = NewCommand(pluginCmd, goSumFlag, goVersionFlag, libcVersionFlag, gogetFlag) rulesToExcludeFlag := StringFlagBuilder(&rulesToExclude, "ignore", "i", rulesToExclude, "List of rules to ignore (comma-separated, no spaces)") severitiesToIncludeFlag := StringFlagBuilder(&severitiesToInclude, "severity", "s", severitiesToInclude, "List of severities to include (comma-separated, no spaces)") pathToRulesToExcludeFlag := StringFlagBuilder(&rulesToExcludePath, "ignore-file", "I", rulesToExcludePath, "Path to a text-plain file containing the list of rules to exclude") formatFlag := StringFlagBuilder(&formatTmpl, "format", "f", formatTmpl, "Inline go template to render the results") - AuditCommand = NewCommand(auditCmd, cfgFlag, rulesToExcludeFlag, severitiesToIncludeFlag, pathToRulesToExcludeFlag, formatFlag) + AuditCommand = NewCommand(auditCmd, cfgFlag, rulesToExcludeFlag, severitiesToIncludeFlag, pathToRulesToExcludeFlag, formatFlag, fixFlag) VersionCommand = NewCommand(versionCmd) diff --git a/testdata/changes/expected.txt b/testdata/changes/expected.txt new file mode 100644 index 0000000..4ed20d7 --- /dev/null +++ b/testdata/changes/expected.txt @@ -0,0 +1,9 @@ +cloud.google.com/go + have: v0.100.3 + want: v0.100.2 +github.com/Azure/azure-sdk-for-go + have: v59.3.1+incompatible + want: v59.3.0+incompatible +golang.org/x/mod + have: v0.6.10-dev.0.20220419223038-86c51ed26bb4 + want: v0.6.0-dev.0.20220419223038-86c51ed26bb4 diff --git a/testdata/changes/go.mod b/testdata/changes/go.mod new file mode 100644 index 0000000..c429cbf --- /dev/null +++ b/testdata/changes/go.mod @@ -0,0 +1,13 @@ +module github.com/krakendio/krakend-cobra/v2 + +go 1.17 + +require ( + github.com/gin-gonic/gin v1.8.2 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 +) + +require ( + cloud.google.com/go v0.100.2 // indirect + github.com/Azure/azure-sdk-for-go v59.3.0+incompatible // indirect +) diff --git a/testdata/changes-go.sum b/testdata/changes/go.sum similarity index 100% rename from testdata/changes-go.sum rename to testdata/changes/go.sum diff --git a/testdata/fixed-all/go.mod b/testdata/fixed-all/go.mod new file mode 100644 index 0000000..73a409d --- /dev/null +++ b/testdata/fixed-all/go.mod @@ -0,0 +1,17 @@ +module github.com/krakendio/krakend-cobra/v2 + +go 1.17 + +require ( + github.com/gin-gonic/gin v1.8.2 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 +) + +require ( + cloud.google.com/go v0.100.2 // indirect + github.com/Azure/azure-sdk-for-go v59.3.0+incompatible // indirect +) + +replace cloud.google.com/go => cloud.google.com/go v0.100.2 + +replace github.com/Azure/azure-sdk-for-go => github.com/Azure/azure-sdk-for-go v59.3.0+incompatible diff --git a/testdata/fixed-some/go.mod b/testdata/fixed-some/go.mod new file mode 100644 index 0000000..73a409d --- /dev/null +++ b/testdata/fixed-some/go.mod @@ -0,0 +1,17 @@ +module github.com/krakendio/krakend-cobra/v2 + +go 1.17 + +require ( + github.com/gin-gonic/gin v1.8.2 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 +) + +require ( + cloud.google.com/go v0.100.2 // indirect + github.com/Azure/azure-sdk-for-go v59.3.0+incompatible // indirect +) + +replace cloud.google.com/go => cloud.google.com/go v0.100.2 + +replace github.com/Azure/azure-sdk-for-go => github.com/Azure/azure-sdk-for-go v59.3.0+incompatible diff --git a/testdata/format/expected.txt b/testdata/format/expected.txt new file mode 100644 index 0000000..79efa61 --- /dev/null +++ b/testdata/format/expected.txt @@ -0,0 +1,3 @@ +go mod edit --replace cloud.google.com/go=cloud.google.com/go@v0.100.2 +go mod edit --replace github.com/Azure/azure-sdk-for-go=github.com/Azure/azure-sdk-for-go@v59.3.0+incompatible +go get golang.org/x/mod@v0.6.0-dev.0.20220419223038-86c51ed26bb4 diff --git a/testdata/match/go.mod b/testdata/match/go.mod new file mode 100644 index 0000000..c429cbf --- /dev/null +++ b/testdata/match/go.mod @@ -0,0 +1,13 @@ +module github.com/krakendio/krakend-cobra/v2 + +go 1.17 + +require ( + github.com/gin-gonic/gin v1.8.2 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 +) + +require ( + cloud.google.com/go v0.100.2 // indirect + github.com/Azure/azure-sdk-for-go v59.3.0+incompatible // indirect +) diff --git a/testdata/match-go.sum b/testdata/match/go.sum similarity index 100% rename from testdata/match-go.sum rename to testdata/match/go.sum