From 8e5490f2f879eafd31fd5c114b3f21285b9ecdbc Mon Sep 17 00:00:00 2001 From: Pranav Gaikwad Date: Thu, 10 Aug 2023 13:22:00 -0400 Subject: [PATCH] :sparkles: add openrewrite subcommand (#18) Signed-off-by: Pranav Gaikwad --- .gitignore | 3 + Dockerfile | 4 +- cmd/analyze.go | 2 +- cmd/container.go | 116 ++++++++++++++++++++++++++++++++++++ cmd/openrewrite.go | 144 +++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 12 +++- cmd/settings.go | 15 ++++- cmd/transform.go | 21 +++++++ env.local.sample | 9 +++ main.go | 4 +- 10 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 .gitignore create mode 100644 cmd/container.go create mode 100644 cmd/openrewrite.go create mode 100644 cmd/transform.go create mode 100644 env.local.sample diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cc3aa7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ + +env.local \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f8c25b0..264e4b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ FROM registry.access.redhat.com/ubi9-minimal as rulesets RUN microdnf -y install git RUN git clone https://github.com/konveyor/rulesets +RUN git clone https://github.com/windup/windup-rulesets # Build the manager binary FROM golang:1.18 as builder @@ -25,9 +26,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o kantra main.go FROM quay.io/konveyor/analyzer-lsp:latest -RUN mkdir /opt/rulesets +RUN mkdir /opt/rulesets /opt/openrewrite /opt/input COPY --from=builder /workspace/kantra /usr/local/bin/kantra COPY --from=shim /usr/bin/windup-shim /usr/local/bin COPY --from=rulesets /rulesets/default/generated /opt/rulesets +COPY --from=rulesets /windup-rulesets/rules/rules-reviewed/openrewrite /opt/openrewrite ENTRYPOINT ["kantra"] diff --git a/cmd/analyze.go b/cmd/analyze.go index a0f4e69..15eb759 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -107,7 +107,7 @@ func AnalyzeFlags() error { func readRuleFilesForLabels(label string) ([]string, error) { var labelsSlice []string - err := filepath.WalkDir(Settings.RuleSetPath, walkRuleSets(Settings.RuleSetPath, label, &labelsSlice)) + err := filepath.WalkDir(RulesetPath, walkRuleSets(RulesetPath, label, &labelsSlice)) if err != nil { return nil, err } diff --git a/cmd/container.go b/cmd/container.go new file mode 100644 index 0000000..37926ed --- /dev/null +++ b/cmd/container.go @@ -0,0 +1,116 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" +) + +type containerCommand struct { + stdout io.Writer + stderr io.Writer + containerName string + containerImage string + entrypointBin string + entrypointArgs []string + workdir string + // map of source -> dest paths to mount + volumes map[string]string +} + +type Option func(c *containerCommand) + +func WithContainerImage(i string) Option { + return func(c *containerCommand) { + c.containerImage = i + } +} + +func WithContainerName(n string) Option { + return func(c *containerCommand) { + c.containerName = n + } +} + +func WithEntrypointBin(b string) Option { + return func(c *containerCommand) { + c.entrypointBin = b + } +} + +func WithEntrypointArgs(args ...string) Option { + return func(c *containerCommand) { + c.entrypointArgs = args + } +} + +func WithWorkDir(w string) Option { + return func(c *containerCommand) { + c.workdir = w + } +} + +func WithVolumes(m map[string]string) Option { + return func(c *containerCommand) { + c.volumes = m + } +} + +func WithStdout(o io.Writer) Option { + return func(c *containerCommand) { + c.stdout = o + } +} + +func WithStderr(e io.Writer) Option { + return func(c *containerCommand) { + c.stderr = e + } +} + +func NewContainerCommand(ctx context.Context, opts ...Option) *exec.Cmd { + c := &containerCommand{ + containerImage: Settings.RunnerImage, + entrypointArgs: []string{}, + volumes: make(map[string]string), + stdout: os.Stdout, + stderr: os.Stderr, + } + + for _, opt := range opts { + opt(c) + } + + args := []string{"run", "-it"} + if c.containerName != "" { + args = append(args, "--name") + args = append(args, c.containerName) + } + + if c.entrypointBin != "" { + args = append(args, "--entrypoint") + args = append(args, c.entrypointBin) + } + + if c.workdir != "" { + args = append(args, "--workdir") + args = append(args, c.workdir) + } + + for sourcePath, destPath := range c.volumes { + args = append(args, "-v") + args = append(args, fmt.Sprintf("%s:%s:Z", sourcePath, destPath)) + } + + args = append(args, c.containerImage) + if len(c.entrypointArgs) > 0 { + args = append(args, c.entrypointArgs...) + } + + cmd := exec.CommandContext(ctx, Settings.PodmanBinary, args...) + cmd.Stdout = c.stdout + cmd.Stderr = c.stderr + return cmd +} diff --git a/cmd/openrewrite.go b/cmd/openrewrite.go new file mode 100644 index 0000000..37c29d0 --- /dev/null +++ b/cmd/openrewrite.go @@ -0,0 +1,144 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/apex/log" + "github.com/spf13/cobra" +) + +type openRewriteCommand struct { + listTargets bool + input string + target string + goal string + miscOpts string +} + +func NewOpenRewriteCommand() *cobra.Command { + openRewriteCmd := &openRewriteCommand{} + + openRewriteCommand := &cobra.Command{ + Use: "openrewrite", + + Short: "Transform application source code using OpenRewrite recipes", + PreRun: func(cmd *cobra.Command, args []string) { + if !cmd.Flags().Lookup("list-targets").Changed { + cmd.MarkFlagRequired("input") + cmd.MarkFlagRequired("target") + } + }, + RunE: func(cmd *cobra.Command, args []string) error { + err := openRewriteCmd.Validate() + if err != nil { + return err + } + err = openRewriteCmd.Run(cmd.Context()) + if err != nil { + log.Errorf("failed to execute openrewrite command", err) + return err + } + return nil + }, + } + openRewriteCommand.Flags().BoolVarP(&openRewriteCmd.listTargets, "list-targets", "l", false, "list all available OpenRewrite recipes") + openRewriteCommand.Flags().StringVarP(&openRewriteCmd.target, "target", "t", "", "target openrewrite recipe to use. Run --list-targets to get a list of packaged recipes.") + openRewriteCommand.Flags().StringVarP(&openRewriteCmd.goal, "goal", "g", "dryRun", "target goal") + openRewriteCommand.Flags().StringVarP(&openRewriteCmd.input, "input", "i", "", "path to application source code directory") + + return openRewriteCommand +} + +func (o *openRewriteCommand) Validate() error { + if o.listTargets { + return nil + } + + stat, err := os.Stat(o.input) + if err != nil { + return err + } + if !stat.IsDir() { + log.Errorf("input path %s is not a directory", o.input) + return err + } + + if o.target == "" { + return fmt.Errorf("target recipe must be specified") + } + + if _, found := recipes[o.target]; !found { + return fmt.Errorf("unsupported target recipe. use --list-targets to get list of all recipes") + } + return nil +} + +type recipe struct { + names []string + path string + description string +} + +var recipes = map[string]recipe{ + "eap8-xml": { + names: []string{"org.jboss.windup.eap8.FacesWebXml"}, + path: "eap8/xml/rewrite.yml", + description: "Transform Faces Web XML for EAP8 migration", + }, + "jakarta-xml": { + names: []string{"org.jboss.windup.jakarta.javax.PersistenceXml"}, + path: "jakarta/javax/xml/rewrite.yml", + description: "Transform Persistence XML for Jakarta migration", + }, + "jakarta-bootstrapping": { + names: []string{"org.jboss.windup.jakarta.javax.BootstrappingFiles"}, + path: "jakarta/javax/bootstrapping/rewrite.yml", + description: "Transform bootstrapping files for Jakarta migration", + }, + "jakarta-imports": { + names: []string{"org.jboss.windup.JavaxToJakarta"}, + path: "jakarta/javax/imports/rewrite.yml", + description: "Transform dependencies and imports for Jakarta migration", + }, + "quarkus-properties": { + names: []string{"org.jboss.windup.sb-quarkus.Properties"}, + path: "quarkus/springboot/properties/rewrite.yml", + description: "Migrate Springboot properties to Quarkus", + }, +} + +func (o *openRewriteCommand) Run(ctx context.Context) error { + if o.listTargets { + fmt.Printf("%-20s\t%s\n", "NAME", "DESCRIPTION") + for name, recipe := range recipes { + fmt.Printf("%-20s\t%s\n", name, recipe.description) + } + return nil + } + + volumes := map[string]string{ + o.input: InputPath, + } + args := []string{ + "-U", "org.openrewrite.maven:rewrite-maven-plugin:run", + fmt.Sprintf("-Drewrite.configLocation=%s/%s", + OpenRewriteRecipesPath, recipes[o.target].path), + fmt.Sprintf("-Drewrite.activeRecipes=%s", + strings.Join(recipes[o.target].names, ",")), + } + cmd := NewContainerCommand( + ctx, + WithEntrypointArgs(args...), + WithEntrypointBin("/usr/bin/mvn"), + WithVolumes(volumes), + WithWorkDir(InputPath), + ) + err := cmd.Run() + if err != nil { + return err + } + return nil +} diff --git a/cmd/root.go b/cmd/root.go index 57ae8d8..3c04e30 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ Copyright © 2023 NAME HERE package cmd import ( + "context" "log" "os" @@ -17,6 +18,10 @@ var rootCmd = &cobra.Command{ Long: ``, } +func init() { + rootCmd.AddCommand(NewOpenRewriteCommand()) +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -25,8 +30,11 @@ func Execute() { log.Fatal("failed to load global settings") } - rootCmd.Use = Settings.CommandName - err = rootCmd.Execute() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + rootCmd.Use = Settings.RootCommandName + err = rootCmd.ExecuteContext(ctx) if err != nil { os.Exit(1) } diff --git a/cmd/settings.go b/cmd/settings.go index 9ab21e0..24196c1 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -1,12 +1,21 @@ package cmd -import "github.com/codingconcepts/env" +import ( + "github.com/codingconcepts/env" +) var Settings = &Config{} +const ( + RulesetPath = "/opt/rulesets" + OpenRewriteRecipesPath = "/opt/openrewrite" + InputPath = "/opt/input" +) + type Config struct { - RuleSetPath string `env:"RULESET_PATH" default:"/opt/rulesets/"` - CommandName string `env:"CMD_NAME" default:"kantra"` + RootCommandName string `env:"CMD_NAME" default:"kantra"` + PodmanBinary string `env:"PODMAN_BIN" default:"/usr/bin/podman"` + RunnerImage string `env:"RUNNER_IMG" default:"quay.io/konveyor/kantra"` } func (c *Config) Load() error { diff --git a/cmd/transform.go b/cmd/transform.go new file mode 100644 index 0000000..d45f0cf --- /dev/null +++ b/cmd/transform.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func NewTransformCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "transform", + + Short: "Transform application source code or windup XML rules", + PreRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + cmd.AddCommand(NewOpenRewriteCommand()) + return cmd +} diff --git a/env.local.sample b/env.local.sample new file mode 100644 index 0000000..a9e6156 --- /dev/null +++ b/env.local.sample @@ -0,0 +1,9 @@ +#!/bin/bash + +# this is a sample file that contains environment variables used by kantra +# by default, values for these variables are set to work in the docker image +# when running locally, you will need to tweak these values as per your env +# copy this file to `env.local` and source it in your bash session to use CLI + +PODMAN_BIN= +RUNNER_IMG= \ No newline at end of file diff --git a/main.go b/main.go index fee1cb2..6374575 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main -import "github.com/konveyor-ecosystem/kantra/cmd" +import ( + "github.com/konveyor-ecosystem/kantra/cmd" +) func main() { cmd.Execute()