Skip to content

Commit

Permalink
[HPR-1190] Add packer-sdc fix command (#190)
Browse files Browse the repository at this point in the history
* packer-sdc: Add fix sub-command

Fix rewrites parts of the plugin codebase to address known issues or
common workarounds used within plugins consuming the Packer plugin SDK.

```
~>  ./packer-sdc fix -check ../../../packer-plugin-tencentcloud
Found the working directory
/Users/wilken/Development/linux-dev/packer-plugin-tencentcloud
gocty Unfixed!

```

* Update fix command

This change is major refactor of the fix command, and it's underlying
fixer interface. This change adds support for the `-diff` flag which
displays the diff between the unfixed and fixed files, if available.

Along with the refactor the ability to apply multiple fixes to the same
file without potential write conflicts where previous changes were
removed after reprocessing.

* Add a scan function that returns a list of files to apply a fix on
to provide flexibility in the future for collecting a list of files.

* Replace cmp.Diff with github.com/pkg/diff for better file diffs
* Return error when missing directory argument
* Add error handling when trying to read any scanned files
  • Loading branch information
nywilken committed Jul 14, 2023
1 parent 2a6d852 commit 7d3a4b2
Show file tree
Hide file tree
Showing 22 changed files with 639 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/packer-sdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ specific help commands:
* `packer-sdc plugin-check -h`
* `packer-sdc struct-markdown -h`
* `packer-sdc renderdocs -h`
* `packer-sdc fix -h`
16 changes: 16 additions & 0 deletions cmd/packer-sdc/internal/fix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## `packer-sdc fix`

Fix rewrites parts of the plugin codebase to address known issues or common workarounds used within plugins consuming the Packer plugin SDK.

Options:

-diff If the -diff flag is set, no files are rewritten. Instead, fix prints the differences a rewrite would introduce.

Available Fixes:

gocty Adds a replace directive for github.com/zclconf/go-cty to github.com/nywilken/go-cty


### Related Issues
Use `packer-sdc fix` to resolve the [cty.Value does not implement gob.GobEncoder](https://github.com/hashicorp/packer-plugin-sdk/issues/187)

158 changes: 158 additions & 0 deletions cmd/packer-sdc/internal/fix/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package fix

import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/pkg/diff"
)

const cmdPrefix string = "fix"

// Command is the base entry for the fix sub-command.
type Command struct {
Dir string
Diff bool
}

// Flags contain the default flags for the fix sub-command.
func (cmd *Command) Flags() *flag.FlagSet {
fs := flag.NewFlagSet(cmdPrefix, flag.ExitOnError)
fs.BoolVar(&cmd.Diff, "diff", false, "if set prints the differences a rewrite would introduce.")
return fs
}

// Help displays usage for the command.
func (cmd *Command) Help() string {
var s strings.Builder
for _, fix := range availableFixes {
s.WriteString(fmt.Sprintf(" %s\t\t%s\n", fix.name, fix.description))
}

helpText := `
Usage: packer-sdc fix [options] directory
Fix rewrites parts of the plugin codebase to address known issues or
common workarounds used within plugins consuming the Packer plugin SDK.
Options:
-diff If the -diff flag is set fix prints the differences an applied fix would introduce.
Available fixes:
%s`
return fmt.Sprintf(helpText, s.String())
}

// Run executes the command
func (cmd *Command) Run(args []string) int {
if err := cmd.run(args); err != nil {
fmt.Printf("%v", err)
return 1
}
return 0
}

func (cmd *Command) run(args []string) error {
f := cmd.Flags()
err := f.Parse(args)
if err != nil {
return errors.New("unable to parse flags for fix command")
}

if f.NArg() != 1 {
err := fmt.Errorf("packer-sdc fix: missing directory argument\n%s", cmd.Help())
return err
}

dir := f.Arg(0)
if dir == "." || dir == "./..." {
dir, _ = os.Getwd()
}

info, err := os.Stat(dir)
if err != nil && os.IsNotExist(err) {
return errors.New("a plugin root directory must be specified or a dot for the current directory")
}

if !info.IsDir() {
return errors.New("a plugin root directory must be specified or a dot for the current directory")
}

dir, err = filepath.Abs(dir)
if err != nil {
return errors.New("unable to determine the absolute path for the provided plugin root directory")
}
cmd.Dir = dir

return processFiles(cmd.Dir, cmd.Diff)
}

func (cmd *Command) Synopsis() string {
return "Rewrites parts of the plugin codebase to address known issues or common workarounds within plugins consuming the Packer plugin SDK."
}

func processFiles(rootDir string, showDiff bool) error {
srcFiles := make(map[string][]byte)
fixedFiles := make(map[string][]byte)

var hasErrors error
for _, f := range availableFixes {
matches, err := f.scan(rootDir)
if err != nil {
return fmt.Errorf("failed to apply %s fix: %s", f.name, err)
}

//matches contains all files to apply the said fix on
for _, filename := range matches {
if _, ok := srcFiles[filename]; !ok {
bs, err := os.ReadFile(filename)
if err != nil {
hasErrors = errors.Join(hasErrors, err)
}
srcFiles[filename] = bytes.Clone(bs)
}

fixedData, ok := fixedFiles[filename]
if !ok {
fixedData = bytes.Clone(srcFiles[filename])
}

fixedData, err := f.fix(filename, fixedData)
if err != nil {
hasErrors = errors.Join(hasErrors, err)
continue
}
if bytes.Equal(fixedData, srcFiles[filename]) {
continue
}
fixedFiles[filename] = bytes.Clone(fixedData)
}
}

if hasErrors != nil {
return hasErrors
}

if showDiff {
for filename, fixedData := range fixedFiles {
diff.Text(filename, filename+"fixed", string(srcFiles[filename]), string(fixedData), os.Stdout)
}
return nil
}

for filename, fixedData := range fixedFiles {
fmt.Println(filename)
info, _ := os.Stat(filename)
os.WriteFile(filename, fixedData, info.Mode())
}

return nil
}
27 changes: 27 additions & 0 deletions cmd/packer-sdc/internal/fix/fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package fix

// Fixer applies all defined fixes on a plugin dir; a Fixer should be idempotent.
// The caller of any fix is responsible for checking if the file context have changed.
type fixer interface {
fix(filename string, data []byte) ([]byte, error)
}

type fix struct {
name, description string
scan func(dir string) ([]string, error)
fixer
}

var (
// availableFixes to apply to a plugin - refer to init func
availableFixes []fix
)

func init() {
availableFixes = []fix{
goctyFix,
}
}
114 changes: 114 additions & 0 deletions cmd/packer-sdc/internal/fix/gocty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package fix

import (
"fmt"
"os"
"path"

"golang.org/x/mod/modfile"
)

const (
sdkPath string = "github.com/hashicorp/packer-plugin-sdk"
oldPath string = "github.com/zclconf/go-cty"
newPath string = "github.com/nywilken/go-cty"
newVersion string = "1.12.1"
modFilename string = "go.mod"
)

var goctyFix = fix{
name: "gocty",
description: "Adds a replace directive for github.com/zclconf/go-cty to github.com/nywilken/go-cty",
scan: modPaths,
fixer: goCtyFix{
OldPath: oldPath,
NewPath: newPath,
NewVersion: newVersion,
},
}

// modPaths scans the incoming dir for potential go.mod files to fix.
func modPaths(dir string) ([]string, error) {
paths := []string{
path.Join(dir, modFilename),
}
return paths, nil

}

type goCtyFix struct {
OldPath, NewPath, NewVersion string
}

func (f goCtyFix) modFileFormattedVersion() string {
return fmt.Sprintf("v%s", f.NewVersion)
}

// Fix applies a replace directive in a projects go.mod file for f.OldPath to f.NewPath.
// This fix applies to the replacement of github.com/zclconf/go-cty, as described in https://github.com/hashicorp/packer-plugin-sdk/issues/187
// The return data contains the data file with the applied fix. In cases where the fix is already applied or not needed the original data is returned.
func (f goCtyFix) fix(modFilePath string, data []byte) ([]byte, error) {
if _, err := os.Stat(modFilePath); err != nil {
return nil, fmt.Errorf("failed to find go.mod file %s", modFilePath)
}

mf, err := modfile.Parse(modFilePath, data, nil)
if err != nil {
return nil, fmt.Errorf("%s: failed to parse go.mod file: %v", modFilePath, err)
}

// fix doesn't apply to go.mod with no module dependencies
if len(mf.Require) == 0 {
return data, nil
}

var requiresSDK, requiresGoCty bool
for _, req := range mf.Require {
if req.Mod.Path == sdkPath {
requiresSDK = true
}
if req.Mod.Path == f.OldPath {
requiresGoCty = true
}

if requiresSDK && requiresGoCty {
break
}
}

if !(requiresSDK && requiresGoCty) {
return data, nil
}

for _, r := range mf.Replace {
if r.Old.Path != f.OldPath {
continue
}

if r.New.Path != f.NewPath {
return nil, fmt.Errorf("%s: found unexpected replace for %s", modFilePath, r.Old.Path)
}

if r.New.Version == f.modFileFormattedVersion() {
return data, nil
}
}

if err := mf.DropReplace(f.OldPath, ""); err != nil {
return nil, fmt.Errorf("%s: failed to drop previously added replacement fix %v", modFilePath, err)
}

commentSuffix := " // added by packer-sdc fix as noted in github.com/hashicorp/packer-plugin-sdk/issues/187"
if err := mf.AddReplace(f.OldPath, "", f.NewPath, f.modFileFormattedVersion()+commentSuffix); err != nil {
return nil, fmt.Errorf("%s: failed to apply go-cty fix: %v", modFilePath, err)
}

newData, err := mf.Format()
if err != nil {
return nil, err
}
return newData, nil
}
Loading

0 comments on commit 7d3a4b2

Please sign in to comment.