diff --git a/Dockerfile b/Dockerfile index a076ebb..6e4d1dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,11 @@ FROM quay.io/konveyor/windup-shim:latest as shim 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 +RUN microdnf -y install git &&\ + git clone https://github.com/konveyor/rulesets &&\ + git clone https://github.com/windup/windup-rulesets -b 6.2.3.Final + +FROM quay.io/konveyor/static-report as static-report # Build the manager binary FROM golang:1.18 as builder @@ -26,12 +28,13 @@ 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 /opt/rulesets/input /opt/openrewrite /opt/input /opt/input/example -RUN touch /opt/input/settings.json /opt/input/output.yaml +RUN mkdir /opt/rulesets /opt/rulesets/input /opt/openrewrite /opt/input /opt/output 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 +COPY --from=static-report /usr/bin/js-bundle-generator /usr/local/bin +COPY --from=static-report /usr/local/static-report /usr/local/static-report ENTRYPOINT ["kantra"] diff --git a/cmd/analyze.go b/cmd/analyze.go index 624c3cc..0efb7f7 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/apex/log" + "github.com/go-logr/logr" "github.com/konveyor/analyzer-lsp/engine" outputv1 "github.com/konveyor/analyzer-lsp/output/v1/konveyor" "github.com/konveyor/analyzer-lsp/provider" @@ -25,27 +26,58 @@ import ( "golang.org/x/exp/slices" ) +var ( + // application source path inside the container + SourceMountPath = filepath.Join(InputPath, "source") + // analyzer config files + ConfigMountPath = filepath.Join(InputPath, "config") + // user provided rules path + RulesMountPath = filepath.Join(RulesetPath, "input") + // paths to files in the container + AnalysisOutputMountPath = filepath.Join(OutputPath, "output.yaml") + DepsOutputMountPath = filepath.Join(OutputPath, "dependencies.yaml") + ProviderSettingsMountPath = filepath.Join(ConfigMountPath, "settings.json") +) + // kantra analyze flags type analyzeCommand struct { - listSources bool - listTargets bool - skipStaticReport bool - sources []string - targets []string - input string - output string - mode string - rules []string + listSources bool + listTargets bool + skipStaticReport bool + analyzeKnownLibraries bool + sources []string + targets []string + input string + output string + mode string + rules []string + + // tempDirs list of temporary dirs created, used for cleanup + tempDirs []string + log logr.Logger + // isFileInput is set when input points to a file and not a dir + isFileInput bool } // analyzeCmd represents the analyze command -func NewAnalyzeCmd() *cobra.Command { - analyzeCmd := &analyzeCommand{} +func NewAnalyzeCmd(log logr.Logger) *cobra.Command { + analyzeCmd := &analyzeCommand{ + log: log, + } analyzeCommand := &cobra.Command{ Use: "analyze", Short: "Analyze application source code", PreRunE: func(cmd *cobra.Command, args []string) error { + // TODO (pgaikwad): this is nasty + if !cmd.Flags().Lookup("list-sources").Changed && + !cmd.Flags().Lookup("list-targets").Changed { + cmd.MarkFlagRequired("input") + cmd.MarkFlagRequired("output") + if err := cmd.ValidateRequiredFlags(); err != nil { + return err + } + } err := analyzeCmd.Validate() if err != nil { return err @@ -54,30 +86,43 @@ func NewAnalyzeCmd() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { if analyzeCmd.listSources || analyzeCmd.listTargets { - err := analyzeCmd.AnalyzeFlags() + err := analyzeCmd.ListLabels(cmd.Context()) if err != nil { - log.Errorf("Failed to execute analyzeFlags", err) + log.V(5).Error(err, "failed to list rule labels") return err } return nil } - err := analyzeCmd.Run(cmd.Context()) + err := analyzeCmd.RunAnalysis(cmd.Context()) + if err != nil { + log.V(5).Error(err, "failed to execute analysis") + return err + } + err = analyzeCmd.GenerateStaticReport(cmd.Context()) + if err != nil { + log.V(5).Error(err, "failed to generate static report") + return err + } + return nil + }, + PostRunE: func(cmd *cobra.Command, args []string) error { + err := analyzeCmd.Clean(cmd.Context()) if err != nil { - log.Errorf("failed to execute analyze command", err) return err } return nil }, } - analyzeCommand.Flags().BoolVar(&analyzeCmd.listSources, "list-sources", false, "List rules for available migration sources") - analyzeCommand.Flags().BoolVar(&analyzeCmd.listTargets, "list-targets", false, "List rules for available migration targets") - analyzeCommand.Flags().StringArrayVarP(&analyzeCmd.sources, "source", "s", []string{}, "Source technology to consider for analysis") - analyzeCommand.Flags().StringArrayVarP(&analyzeCmd.targets, "target", "t", []string{}, "Target technology to consider for analysis") + analyzeCommand.Flags().BoolVar(&analyzeCmd.listSources, "list-sources", false, "list rules for available migration sources") + analyzeCommand.Flags().BoolVar(&analyzeCmd.listTargets, "list-targets", false, "list rules for available migration targets") + analyzeCommand.Flags().StringArrayVarP(&analyzeCmd.sources, "source", "s", []string{}, "source technology to consider for analysis") + analyzeCommand.Flags().StringArrayVarP(&analyzeCmd.targets, "target", "t", []string{}, "target technology to consider for analysis") analyzeCommand.Flags().StringArrayVar(&analyzeCmd.rules, "rules", []string{}, "filename or directory containing rule files") - analyzeCommand.Flags().StringVarP(&analyzeCmd.input, "input", "i", "", "Path to application source code or a binary") - analyzeCommand.Flags().StringVarP(&analyzeCmd.output, "output", "o", "", "Path to the directory for analysis output") - analyzeCommand.Flags().BoolVar(&analyzeCmd.skipStaticReport, "skip-static-report", false, "Do not generate static report") - analyzeCommand.Flags().StringVarP(&analyzeCmd.mode, "mode", "m", "full", "Analysis mode. Must be one of 'full' or 'source-only'") + analyzeCommand.Flags().StringVarP(&analyzeCmd.input, "input", "i", "", "path to application source code or a binary") + analyzeCommand.Flags().StringVarP(&analyzeCmd.output, "output", "o", "", "path to the directory for analysis output") + analyzeCommand.Flags().BoolVar(&analyzeCmd.skipStaticReport, "skip-static-report", false, "do not generate static report") + analyzeCommand.Flags().BoolVar(&analyzeCmd.analyzeKnownLibraries, "analyze-known-libraries", false, "analyze known open-source libraries") + analyzeCommand.Flags().StringVarP(&analyzeCmd.mode, "mode", "m", string(provider.FullAnalysisMode), "analysis mode. Must be one of 'full' or 'source-only'") return analyzeCommand } @@ -88,12 +133,25 @@ func (a *analyzeCommand) Validate() error { } stat, err := os.Stat(a.output) if err != nil { - log.Errorf("failed to stat output directory %s", a.output) - return err + return fmt.Errorf("failed to stat output directory %s", a.output) } if !stat.IsDir() { - log.Errorf("output path %s is not a directory", a.output) - return err + return fmt.Errorf("output path %s is not a directory", a.output) + } + stat, err = os.Stat(a.input) + if err != nil { + return fmt.Errorf("failed to stat input path %s", a.input) + } + // when input isn't a dir, it's pointing to a binary + // we need abs path to mount the file correctly + if !stat.Mode().IsDir() { + a.input, err = filepath.Abs(a.input) + if err != nil { + return fmt.Errorf("failed to get absolute path for input file %s", a.input) + } + // make sure we mount a file and not a dir + SourceMountPath = filepath.Join(SourceMountPath, filepath.Base(a.input)) + a.isFileInput = true } if a.mode != string(provider.FullAnalysisMode) && a.mode != string(provider.SourceOnlyAnalysisMode) { @@ -102,33 +160,58 @@ func (a *analyzeCommand) Validate() error { return nil } -func (a *analyzeCommand) AnalyzeFlags() error { +func (a *analyzeCommand) ListLabels(ctx context.Context) error { // reserved labels sourceLabel := outputv1.SourceTechnologyLabel targetLabel := outputv1.TargetTechnologyLabel - - if a.listSources { - sourceSlice, err := a.readRuleFilesForLabels(sourceLabel) + runMode := "RUN_MODE" + runModeContainer := "container" + if os.Getenv(runMode) == runModeContainer { + if a.listSources { + sourceSlice, err := readRuleFilesForLabels(sourceLabel) + if err != nil { + a.log.V(5).Error(err, "failed to read rule labels") + return err + } + listOptionsFromLabels(sourceSlice, sourceLabel) + return nil + } + if a.listTargets { + targetsSlice, err := readRuleFilesForLabels(targetLabel) + if err != nil { + a.log.V(5).Error(err, "failed to read rule labels") + return err + } + listOptionsFromLabels(targetsSlice, targetLabel) + return nil + } + } else { + volumes, err := a.getRulesVolumes() if err != nil { return err } - listOptionsFromLabels(sourceSlice, sourceLabel) - return nil - } - if a.listTargets { - targetsSlice, err := a.readRuleFilesForLabels(targetLabel) + args := []string{"analyze"} + if a.listSources { + args = append(args, "--list-sources") + } else { + args = append(args, "--list-targets") + } + err = NewContainer().Run( + ctx, + WithEnv(runMode, runModeContainer), + WithVolumes(volumes), + WithEntrypointBin("/usr/local/bin/kantra"), + WithEntrypointArgs(args...), + ) if err != nil { return err } - listOptionsFromLabels(targetsSlice, targetLabel) - return nil } - return nil } -func (a *analyzeCommand) readRuleFilesForLabels(label string) ([]string, error) { - var labelsSlice []string +func readRuleFilesForLabels(label string) ([]string, error) { + labelsSlice := []string{} err := filepath.WalkDir(RulesetPath, walkRuleSets(RulesetPath, label, &labelsSlice)) if err != nil { return nil, err @@ -139,7 +222,7 @@ func (a *analyzeCommand) readRuleFilesForLabels(label string) ([]string, error) func walkRuleSets(root string, label string, labelsSlice *[]string) fs.WalkDirFunc { return func(path string, d fs.DirEntry, err error) error { if !d.IsDir() { - *labelsSlice, err = readRuleFiles(path, labelsSlice, label) + *labelsSlice, err = readRuleFile(path, labelsSlice, label) if err != nil { return err } @@ -148,7 +231,7 @@ func walkRuleSets(root string, label string, labelsSlice *[]string) fs.WalkDirFu } } -func readRuleFiles(filePath string, labelsSlice *[]string, label string) ([]string, error) { +func readRuleFile(filePath string, labelsSlice *[]string, label string) ([]string, error) { file, err := os.Open(filePath) if err != nil { return nil, err @@ -201,25 +284,30 @@ func listOptionsFromLabels(sl []string, label string) { } } -func (a *analyzeCommand) createOutputFile() (string, error) { - fp := filepath.Join(a.output, "output.yaml") - outputFile, err := os.Create(fp) +func (a *analyzeCommand) getConfigVolumes() (map[string]string, error) { + tempDir, err := os.MkdirTemp("", "analyze-config-") if err != nil { - return "", err + return nil, err + } + a.log.V(5).Info("created directory for provider settings", "dir", tempDir) + a.tempDirs = append(a.tempDirs, tempDir) + + otherProvsMountPath := SourceMountPath + // when input is a file, it means it's probably a binary + // only java provider can work with binaries, all others + // continue pointing to the directory instead of file + if a.isFileInput { + otherProvsMountPath = filepath.Dir(otherProvsMountPath) } - defer outputFile.Close() - return fp, nil -} -func (a *analyzeCommand) writeProviderSettings() error { provConfig := []provider.Config{ { Name: "go", BinaryPath: "/usr/bin/generic-external-provider", InitConfig: []provider.InitConfig{ { - Location: "/opt/input/example", - AnalysisMode: provider.FullAnalysisMode, + Location: otherProvsMountPath, + AnalysisMode: provider.AnalysisMode(a.mode), ProviderSpecificConfig: map[string]interface{}{ "name": "go", "dependencyProviderPath": "/usr/bin/golang-dependency-provider", @@ -233,10 +321,11 @@ func (a *analyzeCommand) writeProviderSettings() error { BinaryPath: "/jdtls/bin/jdtls", InitConfig: []provider.InitConfig{ { - Location: "/opt/input/example", - AnalysisMode: provider.SourceOnlyAnalysisMode, + Location: SourceMountPath, + AnalysisMode: provider.AnalysisMode(a.mode), ProviderSpecificConfig: map[string]interface{}{ "bundles": "/jdtls/java-analyzer-bundle/java-analyzer-bundle.core/target/java-analyzer-bundle.core-1.0.0-SNAPSHOT.jar", + "depOpenSourceLabelsFile": "/usr/local/etc/maven.default.index", provider.LspServerPathConfigKey: "/jdtls/bin/jdtls", }, }, @@ -246,32 +335,37 @@ func (a *analyzeCommand) writeProviderSettings() error { Name: "builtin", InitConfig: []provider.InitConfig{ { - Location: "/opt/input/example", - AnalysisMode: "", + Location: otherProvsMountPath, + AnalysisMode: provider.AnalysisMode(a.mode), }, }, }, } - jsonData, err := json.MarshalIndent(&provConfig, "", " ") if err != nil { - return err + return nil, err } - fileName := "settings.json" - err = ioutil.WriteFile(fileName, jsonData, os.ModePerm) + err = ioutil.WriteFile(filepath.Join(tempDir, "settings.json"), jsonData, os.ModePerm) if err != nil { - return err + return nil, err } - return nil + return map[string]string{ + tempDir: ConfigMountPath, + }, nil } -func (a *analyzeCommand) getRules(ruleMountedPath string, wd string, dirName string) (map[string]string, error) { - rulesMap := make(map[string]string) +func (a *analyzeCommand) getRulesVolumes() (map[string]string, error) { + if a.rules == nil || len(a.rules) == 0 { + return nil, nil + } + rulesVolumes := make(map[string]string) rulesetNeeded := false - err := os.Mkdir(dirName, os.ModePerm) + tempDir, err := os.MkdirTemp("", "analyze-rules-") if err != nil { return nil, err } + a.log.V(5).Info("created directory for rules", "dir", tempDir) + a.tempDirs = append(a.tempDirs, tempDir) for i, r := range a.rules { stat, err := os.Stat(r) if err != nil { @@ -281,21 +375,25 @@ func (a *analyzeCommand) getRules(ruleMountedPath string, wd string, dirName str // move rules files passed into dir to mount if !stat.IsDir() { rulesetNeeded = true - destFile := filepath.Join(dirName, fmt.Sprintf("rules%v.yaml", i)) + destFile := filepath.Join(tempDir, fmt.Sprintf("rules%d.yaml", i)) err := copyFileContents(r, destFile) if err != nil { + log.Errorf("failed to move rules file from %s to %s", r, destFile) return nil, err } } else { - dirName = r + rulesVolumes[r] = filepath.Join(RulesMountPath, filepath.Base(r)) } } if rulesetNeeded { - createTempRuleSet(wd, dirName) + err = createTempRuleSet(filepath.Join(tempDir, "ruleset.yaml")) + if err != nil { + log.Error("failed to create ruleset for custom rules") + return nil, err + } + rulesVolumes[tempDir] = filepath.Join(RulesMountPath, filepath.Base(tempDir)) } - // add to volumes - rulesMap[dirName] = ruleMountedPath - return rulesMap, nil + return rulesVolumes, nil } func copyFileContents(src string, dst string) (err error) { @@ -316,7 +414,7 @@ func copyFileContents(src string, dst string) (err error) { return nil } -func createTempRuleSet(wd string, tempDirName string) error { +func createTempRuleSet(path string) error { tempRuleSet := engine.RuleSet{ Name: "ruleset", Description: "temp ruleset", @@ -325,70 +423,197 @@ func createTempRuleSet(wd string, tempDirName string) error { if err != nil { return err } - fileName := "ruleset.yaml" - err = ioutil.WriteFile(fileName, yamlData, os.ModePerm) + err = ioutil.WriteFile(path, yamlData, os.ModePerm) if err != nil { return err } - // move temp ruleset into temp dir - rulsetDefault := filepath.Join(wd, "ruleset.yaml") - destRuleSet := filepath.Join(tempDirName, "ruleset.yaml") - err = copyFileContents(rulsetDefault, destRuleSet) - if err != nil { - return err - } - defer os.Remove(rulsetDefault) return nil } -func (a *analyzeCommand) Run(ctx context.Context) error { - outputFilePath, err := a.createOutputFile() - if err != nil { - return err - } - wd, err := os.Getwd() - if err != nil { - return err +func (a *analyzeCommand) RunAnalysis(ctx context.Context) error { + volumes := map[string]string{ + // application source code + a.input: SourceMountPath, + // output directory + a.output: OutputPath, } - // TODO: clean this up - settingsFilePath := filepath.Join(wd, "settings.json") - settingsMountedPath := filepath.Join(InputPath, "settings.json") - outputMountedPath := filepath.Join(InputPath, "output.yaml") - sourceAppPath := filepath.Join(InputPath, "example") - rulesetName := filepath.Join(RulesetPath, "input") - tempDirName := filepath.Join(wd, "tempRulesDir") - err = a.writeProviderSettings() + + configVols, err := a.getConfigVolumes() if err != nil { + a.log.V(5).Error(err, "failed to get config volumes for analysis") return err } - volumes := map[string]string{ - a.input: sourceAppPath, - settingsFilePath: settingsMountedPath, - outputFilePath: outputMountedPath, - } + maps.Copy(volumes, configVols) + if len(a.rules) > 0 { - ruleVols, err := a.getRules(rulesetName, wd, tempDirName) + ruleVols, err := a.getRulesVolumes() if err != nil { + a.log.V(5).Error(err, "failed to get rule volumes for analysis") return err } maps.Copy(volumes, ruleVols) } + args := []string{ - fmt.Sprintf("--provider-settings=%v", settingsMountedPath), - fmt.Sprintf("--rules=%v", RulesetPath), - fmt.Sprintf("--output-file=%v", outputMountedPath), + fmt.Sprintf("--provider-settings=%s", ProviderSettingsMountPath), + fmt.Sprintf("--rules=%s/", RulesetPath), + fmt.Sprintf("--output-file=%s", AnalysisOutputMountPath), + } + if !a.analyzeKnownLibraries { + args = append(args, + fmt.Sprintf("--dep-label-selector=(!%s=open-source)", provider.DepSourceLabel)) + } + labelSelector := a.getLabelSelector() + if labelSelector != "" { + args = append(args, fmt.Sprintf("--label-selector=%s", labelSelector)) + } + + analysisLogFilePath := filepath.Join(a.output, "analysis.log") + depsLogFilePath := filepath.Join(a.output, "dependency.log") + // create log files + analysisLog, err := os.Create(analysisLogFilePath) + if err != nil { + return fmt.Errorf("failed creating analysis log file at %s", analysisLogFilePath) + } + defer analysisLog.Close() + dependencyLog, err := os.Create(depsLogFilePath) + if err != nil { + return fmt.Errorf("failed creating dependency analysis log file %s", depsLogFilePath) } - cmd := NewContainerCommand( + defer dependencyLog.Close() + + a.log.Info("running source code analysis", + "log", analysisLogFilePath, "input", a.input, "output", a.output, "args", strings.Join(args, " ")) + // TODO (pgaikwad): run analysis & deps in parallel + err = NewContainer().Run( ctx, + WithVolumes(volumes), + WithStdout(os.Stdout, analysisLog), + WithStderr(os.Stdout, analysisLog), WithEntrypointArgs(args...), WithEntrypointBin("/usr/bin/konveyor-analyzer"), + ) + if err != nil { + return err + } + + a.log.Info("running dependency analysis", + "log", depsLogFilePath, "input", a.input, "output", a.output, "args", strings.Join(args, " ")) + err = NewContainer().Run( + ctx, + WithStdout(os.Stdout, dependencyLog), + WithStderr(os.Stderr, dependencyLog), + WithVolumes(volumes), + WithEntrypointBin("/usr/bin/konveyor-analyzer-dep"), + WithEntrypointArgs( + fmt.Sprintf("--output-file=%s", DepsOutputMountPath), + fmt.Sprintf("--provider-settings=%s", ProviderSettingsMountPath), + ), + ) + if err != nil { + return err + } + + return nil +} + +func (a *analyzeCommand) GenerateStaticReport(ctx context.Context) error { + if a.skipStaticReport { + return nil + } + + volumes := map[string]string{ + a.input: SourceMountPath, + a.output: OutputPath, + } + + args := []string{ + fmt.Sprintf("--analysis-output-list=%s", AnalysisOutputMountPath), + fmt.Sprintf("--deps-output-list=%s", DepsOutputMountPath), + fmt.Sprintf("--output-path=%s", filepath.Join("/usr/local/static-report/output.js")), + fmt.Sprintf("--application-name-list=%s", filepath.Base(a.input)), + } + + a.log.Info("generating static report", + "output", a.output, "args", strings.Join(args, " ")) + container := NewContainer() + err := container.Run( + ctx, + WithEntrypointBin("/usr/local/bin/js-bundle-generator"), + WithEntrypointArgs(args...), WithVolumes(volumes), + // keep container to copy static report + WithCleanup(false), ) - err = cmd.Run() if err != nil { return err } - defer os.RemoveAll(tempDirName) - defer os.Remove("settings.json") + + err = container.Cp(ctx, "/usr/local/static-report", a.output) + if err != nil { + return err + } + + err = container.Rm(ctx) + if err != nil { + return err + } + + return nil +} + +func (a *analyzeCommand) Clean(ctx context.Context) error { + for _, path := range a.tempDirs { + err := os.RemoveAll(path) + if err != nil { + a.log.V(5).Error(err, "failed to delete temporary dir", "dir", path) + continue + } + } return nil } + +func (a *analyzeCommand) getLabelSelector() string { + if (a.sources == nil || len(a.sources) == 0) && + (a.targets == nil || len(a.targets) == 0) { + return "" + } + // default labels are applied everytime either a source or target is specified + defaultLabels := []string{"discovery"} + targets := []string{} + for _, target := range a.targets { + targets = append(targets, + fmt.Sprintf("%s=%s", outputv1.TargetTechnologyLabel, target)) + } + sources := []string{} + for _, source := range a.sources { + sources = append(sources, + fmt.Sprintf("%s=%s", outputv1.SourceTechnologyLabel, source)) + } + targetExpr := "" + if len(targets) > 0 { + targetExpr = fmt.Sprintf("(%s)", strings.Join(targets, " || ")) + } + sourceExpr := "" + if len(sources) > 0 { + sourceExpr = fmt.Sprintf("(%s)", strings.Join(sources, " || ")) + } + if targetExpr != "" { + if sourceExpr != "" { + // when both targets and sources are present, AND them + return fmt.Sprintf("(%s && %s) || (%s)", + targetExpr, sourceExpr, strings.Join(defaultLabels, " || ")) + } else { + // when target is specified, but source is not + // use a catch-all expression for source + return fmt.Sprintf("(%s && %s) || (%s)", + targetExpr, outputv1.SourceTechnologyLabel, strings.Join(defaultLabels, " || ")) + } + } + if sourceExpr != "" { + // when only source is specified, OR them all + return fmt.Sprintf("%s || (%s)", + sourceExpr, strings.Join(defaultLabels, " || ")) + } + return "" +} diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go new file mode 100644 index 0000000..b87817b --- /dev/null +++ b/cmd/analyze_test.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "reflect" + "testing" +) + +func Test_analyzeCommand_getLabelSelectorArgs(t *testing.T) { + tests := []struct { + name string + sources []string + targets []string + want string + }{ + { + name: "neither sources nor targets must not create label selector", + }, + { + name: "one target specified, return target, catch-all source and default labels", + targets: []string{"test"}, + want: "((konveyor.io/target=test) && konveyor.io/source) || (discovery)", + }, + { + name: "one source specified, return source and default labels", + sources: []string{"test"}, + want: "(konveyor.io/source=test) || (discovery)", + }, + { + name: "one source & one target specified, return source, target and default labels", + sources: []string{"test"}, + targets: []string{"test"}, + want: "((konveyor.io/target=test) && (konveyor.io/source=test)) || (discovery)", + }, + { + name: "multiple sources specified, OR them all with default labels", + sources: []string{"t1", "t2"}, + want: "(konveyor.io/source=t1 || konveyor.io/source=t2) || (discovery)", + }, + { + name: "multiple targets specified, OR them all, AND result with catch-all source label, finally OR with default labels", + targets: []string{"t1", "t2"}, + want: "((konveyor.io/target=t1 || konveyor.io/target=t2) && konveyor.io/source) || (discovery)", + }, + { + name: "multiple sources & targets specified, OR them within each other, AND result with catch-all source label, finally OR with default labels", + targets: []string{"t1", "t2"}, + sources: []string{"t1", "t2"}, + want: "((konveyor.io/target=t1 || konveyor.io/target=t2) && (konveyor.io/source=t1 || konveyor.io/source=t2)) || (discovery)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &analyzeCommand{ + sources: tt.sources, + targets: tt.targets, + } + if got := a.getLabelSelector(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("analyzeCommand.getLabelSelectorArgs() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/container.go b/cmd/container.go index 37926ed..208e825 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -1,116 +1,228 @@ package cmd import ( + "bytes" "context" "fmt" "io" + "math/rand" "os" "os/exec" + "strings" + "time" ) -type containerCommand struct { - stdout io.Writer - stderr io.Writer - containerName string - containerImage string +type container struct { + stdout []io.Writer + stderr []io.Writer + name string + image string entrypointBin string entrypointArgs []string workdir string + env map[string]string + // whether to delete container after run() + cleanup bool // map of source -> dest paths to mount volumes map[string]string } -type Option func(c *containerCommand) +type Option func(c *container) -func WithContainerImage(i string) Option { - return func(c *containerCommand) { - c.containerImage = i +func WithImage(i string) Option { + return func(c *container) { + c.image = i } } -func WithContainerName(n string) Option { - return func(c *containerCommand) { - c.containerName = n +func WithName(n string) Option { + return func(c *container) { + c.name = n } } func WithEntrypointBin(b string) Option { - return func(c *containerCommand) { + return func(c *container) { c.entrypointBin = b } } func WithEntrypointArgs(args ...string) Option { - return func(c *containerCommand) { + return func(c *container) { c.entrypointArgs = args } } func WithWorkDir(w string) Option { - return func(c *containerCommand) { + return func(c *container) { c.workdir = w } } func WithVolumes(m map[string]string) Option { - return func(c *containerCommand) { + return func(c *container) { c.volumes = m } } -func WithStdout(o io.Writer) Option { - return func(c *containerCommand) { +func WithStdout(o ...io.Writer) Option { + return func(c *container) { c.stdout = o } } -func WithStderr(e io.Writer) Option { - return func(c *containerCommand) { +func WithStderr(e ...io.Writer) Option { + return func(c *container) { c.stderr = e } } -func NewContainerCommand(ctx context.Context, opts ...Option) *exec.Cmd { - c := &containerCommand{ - containerImage: Settings.RunnerImage, +func WithCleanup(cl bool) Option { + return func(c *container) { + c.cleanup = cl + } +} + +func WithEnv(k string, v string) Option { + return func(c *container) { + c.env[k] = v + } +} + +func randomName() string { + rand.Seed(int64(time.Now().Nanosecond())) + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, 16) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +func NewContainer() *container { + return &container{ + image: Settings.RunnerImage, entrypointArgs: []string{}, volumes: make(map[string]string), - stdout: os.Stdout, - stderr: os.Stderr, + stdout: []io.Writer{os.Stdout}, + env: map[string]string{}, + stderr: []io.Writer{os.Stderr}, + name: randomName(), + // by default, remove the container after run() + cleanup: true, } +} + +func (c *container) Exists(ctx context.Context) (bool, error) { + cmd := exec.CommandContext(ctx, + Settings.PodmanBinary, + "ps", "-a", "--format", "{{.Names}}") + output, err := cmd.CombinedOutput() + if err != nil { + return false, fmt.Errorf("failed checking status of container %s", c.name) + } + for _, found := range strings.Split(string(output), "\n") { + if found == c.name { + return true, nil + } + } + return false, nil +} +func (c *container) Run(ctx context.Context, opts ...Option) error { + var err error for _, opt := range opts { opt(c) } - + exists, err := c.Exists(ctx) + if err != nil { + return fmt.Errorf("failed to check status of container %s", c.name) + } + if exists { + return fmt.Errorf("container %s already exists, must remove existing before running", c.name) + } + if c.cleanup { + defer func() { + if rmErr := c.Rm(ctx); rmErr != nil { + err = rmErr + } + }() + } args := []string{"run", "-it"} - if c.containerName != "" { + if c.name != "" { args = append(args, "--name") - args = append(args, c.containerName) + args = append(args, c.name) } - 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) + for k, v := range c.env { + args = append(args, "--env") + args = append(args, fmt.Sprintf("%s=%s", k, v)) + } + args = append(args, c.image) 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 + errBytes := &bytes.Buffer{} + cmd.Stdout = nil + cmd.Stderr = errBytes + if c.stdout != nil { + cmd.Stdout = io.MultiWriter(c.stdout...) + } + if c.stderr != nil { + cmd.Stderr = io.MultiWriter( + append(c.stderr, errBytes)...) + } + err = cmd.Run() + if err != nil { + if _, ok := err.(*exec.ExitError); ok { + return fmt.Errorf(errBytes.String()) + } + return err + } + return nil +} + +func (c *container) Cp(ctx context.Context, src string, dest string) error { + if src == "" || dest == "" { + return fmt.Errorf("source or dest cannot be empty") + } + exists, err := c.Exists(ctx) + if err != nil { + return fmt.Errorf("failed to check status of container %s", c.name) + } + if !exists { + return fmt.Errorf("container %s does not exist, cannot copy from non-existing container", c.name) + } + cmd := exec.CommandContext( + ctx, + Settings.PodmanBinary, + "cp", fmt.Sprintf("%s:%s", c.name, src), dest) + return cmd.Run() +} + +func (c *container) Rm(ctx context.Context) error { + exists, err := c.Exists(ctx) + if err != nil { + return fmt.Errorf("failed to check status of container %s", c.name) + } + if !exists { + return nil + } + cmd := exec.CommandContext( + ctx, + Settings.PodmanBinary, + "rm", c.name) + return cmd.Run() } diff --git a/cmd/openrewrite.go b/cmd/openrewrite.go index 37c29d0..ef8358d 100644 --- a/cmd/openrewrite.go +++ b/cmd/openrewrite.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/apex/log" + "github.com/go-logr/logr" "github.com/spf13/cobra" ) @@ -16,10 +16,13 @@ type openRewriteCommand struct { target string goal string miscOpts string + log logr.Logger } -func NewOpenRewriteCommand() *cobra.Command { - openRewriteCmd := &openRewriteCommand{} +func NewOpenRewriteCommand(log logr.Logger) *cobra.Command { + openRewriteCmd := &openRewriteCommand{ + log: log, + } openRewriteCommand := &cobra.Command{ Use: "openrewrite", @@ -34,11 +37,12 @@ func NewOpenRewriteCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { err := openRewriteCmd.Validate() if err != nil { + log.V(5).Error(err, "failed validating input args") return err } err = openRewriteCmd.Run(cmd.Context()) if err != nil { - log.Errorf("failed to execute openrewrite command", err) + log.V(5).Error(err, "failed executing openrewrite recipe") return err } return nil @@ -59,11 +63,10 @@ func (o *openRewriteCommand) Validate() error { stat, err := os.Stat(o.input) if err != nil { - return err + return fmt.Errorf("failed to stat input directory %s", o.input) } if !stat.IsDir() { - log.Errorf("input path %s is not a directory", o.input) - return err + return fmt.Errorf("input path %s is not a directory", o.input) } if o.target == "" { @@ -129,14 +132,15 @@ func (o *openRewriteCommand) Run(ctx context.Context) error { fmt.Sprintf("-Drewrite.activeRecipes=%s", strings.Join(recipes[o.target].names, ",")), } - cmd := NewContainerCommand( + o.log.Info("executing openrewrite recipe", + "recipe", o.target, "input", o.input, "args", strings.Join(args, " ")) + err := NewContainer().Run( ctx, WithEntrypointArgs(args...), WithEntrypointBin("/usr/bin/mvn"), WithVolumes(volumes), WithWorkDir(InputPath), ) - err := cmd.Run() if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 04f7920..a347cd2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,19 +8,32 @@ import ( "log" "os" + "github.com/bombsimon/logrusr/v3" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ // TODO: better descriptions - Short: "A cli tool for analysis and transformation of applications", - Long: ``, + Short: "A cli tool for analysis and transformation of applications", + Long: ``, + SilenceUsage: true, } +var logLevel int + func init() { - rootCmd.AddCommand(NewOpenRewriteCommand()) - rootCmd.AddCommand(NewAnalyzeCmd()) + rootCmd.PersistentFlags().IntVar(&logLevel, "log-level", 5, "log level") + + logrusLog := logrus.New() + logrusLog.SetOutput(os.Stdout) + logrusLog.SetFormatter(&logrus.TextFormatter{}) + logrusLog.SetLevel(logrus.Level(logLevel)) + log := logrusr.New(logrusLog) + + rootCmd.AddCommand(NewTransformCommand(log)) + rootCmd.AddCommand(NewAnalyzeCmd(log)) } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/settings.go b/cmd/settings.go index 24196c1..a77dbcb 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -10,6 +10,7 @@ const ( RulesetPath = "/opt/rulesets" OpenRewriteRecipesPath = "/opt/openrewrite" InputPath = "/opt/input" + OutputPath = "/opt/output" ) type Config struct { diff --git a/cmd/transform.go b/cmd/transform.go index d45f0cf..da34c06 100644 --- a/cmd/transform.go +++ b/cmd/transform.go @@ -1,10 +1,11 @@ package cmd import ( + "github.com/go-logr/logr" "github.com/spf13/cobra" ) -func NewTransformCommand() *cobra.Command { +func NewTransformCommand(log logr.Logger) *cobra.Command { cmd := &cobra.Command{ Use: "transform", @@ -16,6 +17,6 @@ func NewTransformCommand() *cobra.Command { cmd.Help() }, } - cmd.AddCommand(NewOpenRewriteCommand()) + cmd.AddCommand(NewOpenRewriteCommand(log)) return cmd } diff --git a/go.mod b/go.mod index fa5025a..610ce76 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,15 @@ module github.com/konveyor-ecosystem/kantra go 1.18 -require github.com/spf13/cobra v1.7.0 +require ( + github.com/go-logr/logr v1.2.3 + github.com/spf13/cobra v1.7.0 + gopkg.in/yaml.v2 v2.4.0 +) require ( - github.com/cbroglie/mustache v1.4.0 // indirect + github.com/cbroglie/mustache v1.3.0 // indirect github.com/getkin/kin-openapi v0.108.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect @@ -28,17 +31,18 @@ require ( google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/PaesslerAG/gval v1.2.2 // indirect github.com/apex/log v1.9.0 + github.com/bombsimon/logrusr/v3 v3.1.0 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/konveyor/analyzer-lsp v0.0.0-20230803121037-3fab15f20470 + github.com/konveyor/analyzer-lsp v0.3.0-alpha.1 github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 // indirect golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 ) diff --git a/go.sum b/go.sum index 9e1c04b..7234076 100644 --- a/go.sum +++ b/go.sum @@ -9,9 +9,10 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZA82TQ= -github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= -github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= +github.com/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ= +github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= +github.com/cbroglie/mustache v1.3.0 h1:sj24GVYl8G7MH4b3zaROGsZnF8X79JqtjMx8/6H/nXM= +github.com/cbroglie/mustache v1.3.0/go.mod h1:w58RIHjw/L7DPyRX2CcCTduNmcP1dvztaHP72ciSfh0= github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 h1:5/aEFreBh9hH/0G+33xtczJCvMaulqsm9nDuu2BZUEo= github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482/go.mod h1:TM9ug+H/2cI3EjyIDr5xKCkFGyNE59URgH1wu5NyU8E= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -51,8 +52,8 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= -github.com/konveyor/analyzer-lsp v0.0.0-20230803121037-3fab15f20470 h1:aWhrzEcXC72adAB6GMV1LxDAvwqOp4Ih9abV2j/14do= -github.com/konveyor/analyzer-lsp v0.0.0-20230803121037-3fab15f20470/go.mod h1:+k6UreVv8ztI29/RyQN8/71AAmB0aWwQoWwZd3yR8sc= +github.com/konveyor/analyzer-lsp v0.3.0-alpha.1 h1:RilOnB9E6+zDSDQs4vBVPMITuSD3o+6P0dkXWW2H3YQ= +github.com/konveyor/analyzer-lsp v0.3.0-alpha.1/go.mod h1:Rv2WcWfVMEGEWqn0Fl4U4NcmJYPrmWdPtaFE9KDVVF8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= @@ -81,7 +82,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= @@ -95,6 +97,7 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -130,6 +133,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 1dde1e3..6374575 100644 --- a/main.go +++ b/main.go @@ -6,5 +6,4 @@ import ( func main() { cmd.Execute() - }