From 913bcffa9c2469d85639aae869fb39c7a0d4fe31 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Tue, 12 Sep 2023 05:21:10 -0700 Subject: [PATCH] :ghost: Add ruleset patch tooling. Signed-off-by: Jeff Ortel --- Makefile | 18 +- cmd/{prepare.go => prepare/main.go} | 0 cmd/ruleset/main.go | 516 ++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 2 + 5 files changed, 534 insertions(+), 5 deletions(-) rename cmd/{prepare.go => prepare/main.go} (100%) create mode 100644 cmd/ruleset/main.go diff --git a/Makefile b/Makefile index 10ac161..64b5d9f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,14 @@ -BUILD = -o bin/prepare github.com/konveyor/tackle2-seed/cmd +PREPARE = -o bin/prepare github.com/konveyor/tackle2-seed/cmd/prepare +RULESET = -o bin/ruleset github.com/konveyor/tackle2-seed/cmd/ruleset PKG = ./cmd/... ./pkg/... -cmd: fmt vet - go build ${BUILD} +cmd: prepare ruleset + +prepare: fmt vet + go build ${PREPARE} + +ruleset: fmt vet + go build ${RULESET} fmt: go fmt ${PKG} @@ -10,5 +16,9 @@ fmt: vet: go vet ${PKG} -prepare: cmd +run-prepare: prepare + bin/prepare resources resources + +ruleset-patch: ruleset + bin/ruleset -p resources bin/prepare resources resources diff --git a/cmd/prepare.go b/cmd/prepare/main.go similarity index 100% rename from cmd/prepare.go rename to cmd/prepare/main.go diff --git a/cmd/ruleset/main.go b/cmd/ruleset/main.go new file mode 100644 index 0000000..117cf4f --- /dev/null +++ b/cmd/ruleset/main.go @@ -0,0 +1,516 @@ +package main + +import ( + "bufio" + "encoding/hex" + "fmt" + "github.com/konveyor/tackle2-seed/pkg" + "github.com/pborman/getopt/v2" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" +) + +const ( + Resources = "resources" + RuleSets = "rulesets" + RemoteRuleSets = "default/generated" +) + +var Deps = []string{ + "rulesets/00-discovery", + "rulesets/technology-usage", +} + +var YesAssumed = true + +func main() { + cmd := Cmd{} + err := cmd.Main() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} + +type Cmd struct { + Path string + Remote Remote + Manifest struct { + Current Manifest + Remote Manifest + } +} + +func (r *Cmd) Main() (err error) { + resources := getopt.StringLong( + "path", + 'p', + "./"+Resources, + "The resources path.") + remote := getopt.StringLong( + "remote", + 'r', + "https://github.com/konveyor/rulesets", + "The remote (ruleset) github repository URL. May be file path.") + ref := getopt.StringLong( + "branch", + 'b', + "", + "The github branch (any ref).") + yes := getopt.BoolLong( + "yes", + 'y', + "Yes assumed.") + help := getopt.BoolLong( + "help", + 'h', + "Show help.") + + getopt.Parse() + if *help { + getopt.Usage() + return + } + + YesAssumed = *yes + r.Path = *resources + r.Remote.URL = *remote + r.Remote.Ref = *ref + r.Manifest.Current.Root = *resources + + fmt.Printf("\nResources: %s\n", *resources) + fmt.Printf("Remote: %s\n", *remote) + fmt.Printf("Ref: %s\n", *ref) + + err = r.Manifest.Current.Load() + if err != nil { + return + } + err = r.Remote.Load() + if err != nil { + return + } + err = r.Reconcile() + if err != nil { + return + } + + fmt.Println("\nDone") + return +} + +func (r *Cmd) Reconcile() (err error) { + bash := Bash{} + tmpDir, err := ioutil.TempDir("", "ruleset-*") + if err != nil { + return + } + remote := path.Join(r.Remote.Path, RemoteRuleSets) + dest := path.Join(tmpDir, RuleSets) + err = bash.Run("cp", "-r", remote, dest) + if err != nil { + return + } + r.Manifest.Remote.Root = tmpDir + err = r.Manifest.Remote.Build() + if err != nil { + return + } + r.Manifest.Current.Reconcile(r.Manifest.Remote) + r.Manifest.Current.PrintChanged() + if !r.Manifest.Current.Dirty() { + return + } + b := Ask("Apply?") + if b { + err = r.Apply() + if err != nil { + return + } + } + return +} + +func (r *Cmd) Apply() (err error) { + manifest := r.Manifest.Current + for _, ruleSet := range manifest.changed.added { + err = r.ReplaceDir(ruleSet) + if err != nil { + return + } + } + for _, ruleSet := range manifest.changed.updated { + err = r.ReplaceDir(ruleSet) + if err != nil { + return + } + } + for _, ruleSet := range manifest.changed.deleted { + err = r.Delete(ruleSet) + if err != nil { + return + } + } + err = r.Manifest.Current.Write() + if err != nil { + return + } + bash := Bash{} + bash.Dir = r.Path + err = bash.Run("git add", RuleSets) + if err != nil { + return + } + return +} + +func (r *Cmd) ReplaceDir(ruleSet *pkg.RuleSet) (err error) { + bash := Bash{Silent: true} + remote := path.Join(r.Manifest.Remote.Root, ruleSet.Dir()) + current := path.Join(r.Path, ruleSet.Dir()) + err = bash.Run("rm -rf", current) + if err != nil { + return + } + bash = Bash{} + err = bash.Run("cp -r", remote, current) + return +} +func (r *Cmd) Delete(ruleSet *pkg.RuleSet) (err error) { + bash := Bash{} + p := path.Join(r.Path, ruleSet.Dir()) + err = bash.Run("rm -rf", p) + return +} + +type Remote struct { + URL string + Ref string + Path string +} + +func (r *Remote) Load() (err error) { + if strings.HasPrefix(r.URL, "/") { + r.Path = r.URL + return + } + r.Path, err = ioutil.TempDir("", "ruleset-*") + if err != nil { + return + } + bash := Bash{} + err = bash.Run("git clone", r.URL, r.Path) + if err != nil { + return + } + if r.Ref != "" { + bash.Dir = r.Path + err = bash.Run("git checkout", r.Ref) + if err != nil { + return + } + } + return +} + +type Manifest struct { + Root string + pkg.Seed `yaml:",inline"` + ruleSets []*pkg.RuleSet + dirMap map[string]*pkg.RuleSet + changed struct { + skipped map[string]*pkg.RuleSet + added []*pkg.RuleSet + updated []*pkg.RuleSet + deleted []*pkg.RuleSet + } +} + +func (r *Manifest) Load() (err error) { + r.dirMap = make(map[string]*pkg.RuleSet) + p := path.Join(r.Root, "rulesets.yaml") + fmt.Printf("[Manifest] Read: %s\n", p) + f, err := os.Open(p) + if err != nil { + return + } + defer func() { + _ = f.Close() + }() + d := yaml.NewDecoder(f) + err = d.Decode(r) + if err != nil { + return + } + for _, node := range r.Items { + ruleSet := &pkg.RuleSet{} + err = node.Decode(ruleSet) + if err == nil { + r.ruleSets = append(r.ruleSets, ruleSet) + r.dirMap[ruleSet.Dir()] = ruleSet + } else { + return + } + } + return +} +func (r *Manifest) Write() (err error) { + p := path.Join(r.Root, "rulesets.yaml") + fmt.Printf("[Manifest] Write: %s\n", p) + r.Items = []yaml.Node{} + for _, ruleSet := range r.ruleSets { + node := yaml.Node{} + err = node.Encode(ruleSet) + if err != nil { + return + } + r.Items = append(r.Items, node) + } + f, err := os.Create(p) + if err != nil { + return + } + defer func() { + _ = f.Close() + }() + encoder := yaml.NewEncoder(f) + err = encoder.Encode(r) + return +} + +func (r *Manifest) Build() (err error) { + r.dirMap = make(map[string]*pkg.RuleSet) + p := path.Join(r.Root, RuleSets) + entries, err := os.ReadDir(p) + if err != nil { + return + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + ruleSet := &pkg.RuleSet{ + Directory: path.Join(RuleSets, entry.Name()), + } + err = r.SetName(ruleSet) + if err != nil { + return + } + err = r.SetDigest(ruleSet) + if err != nil { + return + } + r.SetDeps(ruleSet) + r.ruleSets = append(r.ruleSets, ruleSet) + r.dirMap[ruleSet.Dir()] = ruleSet + } + return +} + +func (r *Manifest) Reconcile(other Manifest) { + r.changed.skipped = make(map[string]*pkg.RuleSet) + for _, ruleSet := range other.ruleSets { + if r.IsDep(ruleSet) { + r.changed.skipped[ruleSet.Dir()] = ruleSet + continue + } + matched, found := r.dirMap[ruleSet.Dir()] + if !found { + b := Ask("RuleSet at: %s unknown. Add?", ruleSet.Dir()) + if b { + r.Add(ruleSet) + r.changed.added = append( + r.changed.added, + ruleSet) + } + } else { + if matched.Checksum != ruleSet.Checksum { + b := Ask("RuleSet at: %s changed. Update?", ruleSet.Dir()) + if b { + r.Update(ruleSet) + r.changed.updated = append( + r.changed.updated, + ruleSet) + } + } + } + } + for _, ruleSet := range r.ruleSets { + if r.IsDep(ruleSet) { + r.changed.skipped[ruleSet.Dir()] = ruleSet + continue + } + if _, found := other.dirMap[ruleSet.Dir()]; !found { + b := Ask( + "RuleSet at: %s not-found. Delete?", + ruleSet.Dir()) + if b { + r.Delete(ruleSet) + r.changed.deleted = append( + r.changed.deleted, + ruleSet) + } + } + } +} + +func (r *Manifest) Add(ruleSet *pkg.RuleSet) { + r.ruleSets = append( + r.ruleSets, + ruleSet) +} + +func (r *Manifest) Update(ruleSet *pkg.RuleSet) { + for i := range r.ruleSets { + if r.ruleSets[i].Dir() == ruleSet.Dir() { + r.ruleSets[i].Name = ruleSet.Name + break + } + } +} + +func (r *Manifest) Delete(ruleSet *pkg.RuleSet) { + var wanted []*pkg.RuleSet + for i := range r.ruleSets { + if r.ruleSets[i].Dir() != ruleSet.Dir() { + wanted = append( + wanted, + r.ruleSets[i]) + } + } + r.ruleSets = wanted +} + +func (r *Manifest) SetName(ruleSet *pkg.RuleSet) (err error) { + p := path.Join(r.Root, ruleSet.Dir(), "ruleset.yaml") + object := struct { + Name string + }{} + f, err := os.Open(p) + if err != nil { + if os.IsNotExist(err) { + ruleSet.Name = path.Base(ruleSet.Dir()) + err = nil + } + return + } + defer func() { + _ = f.Close() + }() + b, err := ioutil.ReadAll(f) + if err != nil { + return + } + err = yaml.Unmarshal(b, &object) + if err != nil { + return + } + ruleSet.Name = object.Name + return +} + +func (r *Manifest) SetDigest(ruleSet *pkg.RuleSet) (err error) { + p := path.Join(r.Root, ruleSet.Dir()) + b, err := pkg.ChecksumDir(p) + if err == nil { + ruleSet.Checksum = hex.EncodeToString(b) + } + return +} + +func (r *Manifest) SetDeps(ruleSet *pkg.RuleSet) { + for _, d := range Deps { + ruleSet.Dependencies = append( + ruleSet.Dependencies, + "@"+d) + } +} + +func (r *Manifest) IsDep(ruleSet *pkg.RuleSet) (b bool) { + for _, d := range Deps { + if d == ruleSet.Dir() { + b = true + break + } + } + return +} + +func (r *Manifest) PrintChanged() { + fmt.Printf( + "\n[Manifest] summary: (S)kipped=%d,(A)dded=%d,(M)odified=%d,(D)eleted=%d\n", + len(r.changed.skipped), + len(r.changed.added), + len(r.changed.updated), + len(r.changed.deleted)) + for _, ruleSet := range r.changed.skipped { + fmt.Printf(" S (%s) %s\n", ruleSet.Name, ruleSet.Dir()) + } + for _, ruleSet := range r.changed.added { + fmt.Printf(" A (%s) %s\n", ruleSet.Name, ruleSet.Dir()) + } + for _, ruleSet := range r.changed.updated { + fmt.Printf(" M (%s) %s\n", ruleSet.Name, ruleSet.Dir()) + } + for _, ruleSet := range r.changed.deleted { + fmt.Printf(" D (%s) %s\n", ruleSet.Name, ruleSet.Dir()) + } + fmt.Println("") +} + +func (r *Manifest) Dirty() (b bool) { + n := 0 + n += len(r.changed.added) + n += len(r.changed.updated) + n += len(r.changed.deleted) + b = n > 0 + return +} + +type Bash struct { + Dir string + Silent bool +} + +func (r *Bash) Run(args ...string) (err error) { + command := strings.Join(args, " ") + if !r.Silent { + fmt.Printf("[CMD] %s\n", command) + } + cmd := exec.Command("bash", "--norc", "-c", command) + cmd.Dir = r.Dir + output, err := cmd.CombinedOutput() + if err != nil { + return + } + if !r.Silent { + fmt.Print(string(output)) + } + return +} + +func Ask(prompt string, v ...any) (b bool) { + if YesAssumed { + b = true + return + } + fmt.Printf(prompt, v...) + for { + fmt.Print(" [Y|n]: ") + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + if answer != "" { + switch answer[0] { + case '\n', 'Y', 'y': + b = true + return + case 'N', 'n': + return + } + } + } +} diff --git a/go.mod b/go.mod index 30eb4fc..e068cc5 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module github.com/konveyor/tackle2-seed -go 1.19 +go 1.18 require ( github.com/google/uuid v1.3.0 github.com/jortel/go-utils v0.1.1 + github.com/pborman/getopt/v2 v2.1.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 13bee82..73b3bdf 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= +github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=