diff --git a/cmd/analyze.go b/cmd/analyze.go index b5eae0ca..ed27dcdd 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "gopkg.in/yaml.v2" "io" "io/fs" "os" @@ -18,17 +19,14 @@ import ( "slices" "strings" - "github.com/devfile/alizer/pkg/apis/model" "github.com/devfile/alizer/pkg/apis/recognizer" "github.com/go-logr/logr" "github.com/konveyor-ecosystem/kantra/cmd/internal/hiddenfile" "github.com/konveyor-ecosystem/kantra/pkg/container" - "github.com/konveyor/analyzer-lsp/engine" outputv1 "github.com/konveyor/analyzer-lsp/output/v1/konveyor" "github.com/konveyor/analyzer-lsp/provider" "github.com/phayes/freeport" "go.lsp.dev/uri" - "gopkg.in/yaml.v2" "github.com/spf13/cobra" "golang.org/x/exp/maps" @@ -135,33 +133,18 @@ type analyzeCommand struct { depFolders []string overrideProviderSettings string provider []string - providersMap map[string]ProviderInit - - // 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 - needsBuiltin bool - logLevel *uint32 - // used for cleanup - networkName string - volumeName string - providerContainerNames []string - cleanup bool - runLocal bool - - // for containerless cmd - reqMap map[string]string - kantraDir string + logLevel *uint32 + cleanup bool + runLocal bool + AnalyzeCommandContext } // analyzeCmd represents the analyze command func NewAnalyzeCmd(log logr.Logger) *cobra.Command { analyzeCmd := &analyzeCommand{ - log: log, cleanup: true, } + analyzeCmd.log = log analyzeCommand := &cobra.Command{ Use: "analyze", @@ -250,7 +233,7 @@ func NewAnalyzeCmd(log logr.Logger) *cobra.Command { if analyzeCmd.isFileInput { foundProviders = append(foundProviders, javaProvider) } else { - foundProviders, err = analyzeCmd.setProviders(languages, foundProviders) + foundProviders, err = analyzeCmd.setProviders(analyzeCmd.provider, languages, foundProviders) if err != nil { log.Error(err, "failed to set provider info") return err @@ -310,7 +293,7 @@ func NewAnalyzeCmd(log logr.Logger) *cobra.Command { return err } // share source app with provider and engine containers - containerVolName, err := analyzeCmd.createContainerVolume() + containerVolName, err := analyzeCmd.createContainerVolume(analyzeCmd.input) if err != nil { log.Error(err, "failed to create container volume") return err @@ -366,9 +349,9 @@ func NewAnalyzeCmd(log logr.Logger) *cobra.Command { analyzeCommand.Flags().BoolVar(&analyzeCmd.bulk, "bulk", false, "running multiple analyze commands in bulk will result to combined static report") analyzeCommand.Flags().StringVar(&analyzeCmd.jaegerEndpoint, "jaeger-endpoint", "", "jaeger endpoint to collect traces") analyzeCommand.Flags().BoolVar(&analyzeCmd.enableDefaultRulesets, "enable-default-rulesets", true, "run default rulesets with analysis") - analyzeCommand.Flags().StringVar(&analyzeCmd.httpProxy, "http-proxy", loadEnvInsensitive("http_proxy"), "HTTP proxy string URL") - analyzeCommand.Flags().StringVar(&analyzeCmd.httpsProxy, "https-proxy", loadEnvInsensitive("https_proxy"), "HTTPS proxy string URL") - analyzeCommand.Flags().StringVar(&analyzeCmd.noProxy, "no-proxy", loadEnvInsensitive("no_proxy"), "proxy excluded URLs (relevant only with proxy)") + analyzeCommand.Flags().StringVar(&analyzeCmd.httpProxy, "http-proxy", LoadEnvInsensitive("http_proxy"), "HTTP proxy string URL") + analyzeCommand.Flags().StringVar(&analyzeCmd.httpsProxy, "https-proxy", LoadEnvInsensitive("https_proxy"), "HTTPS proxy string URL") + analyzeCommand.Flags().StringVar(&analyzeCmd.noProxy, "no-proxy", LoadEnvInsensitive("no_proxy"), "proxy excluded URLs (relevant only with proxy)") analyzeCommand.Flags().IntVar(&analyzeCmd.contextLines, "context-lines", 100, "number of lines of source code to include in the output for each incident") analyzeCommand.Flags().StringVar(&analyzeCmd.incidentSelector, "incident-selector", "", "an expression to select incidents based on custom variables. ex: (!package=io.konveyor.demo.config-utils)") analyzeCommand.Flags().StringArrayVarP(&analyzeCmd.depFolders, "dependency-folders", "d", []string{}, "directory for dependencies") @@ -559,89 +542,6 @@ func (a *analyzeCommand) CheckOverwriteOutput() error { return nil } -func (a *analyzeCommand) setProviders(languages []model.Language, foundProviders []string) ([]string, error) { - if len(a.provider) > 0 { - for _, p := range a.provider { - foundProviders = append(foundProviders, p) - return foundProviders, nil - } - } - for _, l := range languages { - if l.CanBeComponent { - a.log.V(5).Info("Got language", "component language", l) - if l.Name == "C#" { - for _, item := range l.Frameworks { - supported, ok := DotnetFrameworks[item] - if ok { - if !supported { - err := fmt.Errorf("Unsupported .NET Framework version") - a.log.Error(err, ".NET Framework version must be greater or equal 'v4.5'") - return foundProviders, err - } - return []string{dotnetFrameworkProvider}, nil - } - } - foundProviders = append(foundProviders, dotnetProvider) - continue - } - if l.Name == "JavaScript" { - for _, item := range l.Tools { - if item == "NodeJs" || item == "Node.js" || item == "nodejs" { - foundProviders = append(foundProviders, nodeJSProvider) - // only need one instance of provider - break - } - } - } else { - foundProviders = append(foundProviders, strings.ToLower(l.Name)) - } - } - } - return foundProviders, nil -} - -func (a *analyzeCommand) setProviderInitInfo(foundProviders []string) error { - for _, prov := range foundProviders { - port, err := freeport.GetFreePort() - if err != nil { - return err - } - switch prov { - case javaProvider: - a.providersMap[javaProvider] = ProviderInit{ - port: port, - image: Settings.JavaProviderImage, - provider: &JavaProvider{}, - } - case goProvider: - a.providersMap[goProvider] = ProviderInit{ - port: port, - image: Settings.GenericProviderImage, - provider: &GoProvider{}, - } - case pythonProvider: - a.providersMap[pythonProvider] = ProviderInit{ - port: port, - image: Settings.GenericProviderImage, - provider: &PythonProvider{}, - } - case nodeJSProvider: - a.providersMap[nodeJSProvider] = ProviderInit{ - port: port, - image: Settings.GenericProviderImage, - provider: &NodeJsProvider{}, - } - case dotnetProvider: - a.providersMap[dotnetProvider] = ProviderInit{ - port: port, - image: Settings.DotnetProviderImage, - provider: &DotNetProvider{}, - } - } - } - return nil -} - func (a *analyzeCommand) validateProviders(providers []string) error { validProvs := []string{ javaProvider, @@ -922,174 +822,6 @@ func (a *analyzeCommand) getRulesVolumes() (map[string]string, error) { return rulesVolumes, nil } -func (a *analyzeCommand) handleDir(p string, tempDir string, basePath string) error { - newDir, err := filepath.Rel(basePath, p) - if err != nil { - return err - } - tempDir = filepath.Join(tempDir, newDir) - a.log.Info("creating nested tmp dir", "tempDir", tempDir, "newDir", newDir) - err = os.Mkdir(tempDir, 0777) - if err != nil { - return err - } - a.log.V(5).Info("create temp rule set for dir", "dir", tempDir) - err = a.createTempRuleSet(tempDir, filepath.Base(p)) - if err != nil { - a.log.V(1).Error(err, "failed to create temp ruleset", "path", tempDir) - return err - } - return err -} - -func copyFolderContents(src string, dst string) error { - err := os.MkdirAll(dst, os.ModePerm) - if err != nil { - return err - } - source, err := os.Open(src) - if err != nil { - return err - } - defer source.Close() - - contents, err := source.Readdir(-1) - if err != nil { - return err - } - - for _, item := range contents { - sourcePath := filepath.Join(src, item.Name()) - destinationPath := filepath.Join(dst, item.Name()) - - if item.IsDir() { - // Recursively copy subdirectories - if err := copyFolderContents(sourcePath, destinationPath); err != nil { - return err - } - } else { - // Copy file - if err := CopyFileContents(sourcePath, destinationPath); err != nil { - return err - } - } - } - - return nil -} - -func CopyFileContents(src string, dst string) (err error) { - source, err := os.Open(src) - if err != nil { - return nil - } - defer source.Close() - destination, err := os.Create(dst) - if err != nil { - return err - } - defer destination.Close() - _, err = io.Copy(destination, source) - if err != nil { - return err - } - return nil -} - -func (a *analyzeCommand) createTempRuleSet(path string, name string) error { - a.log.Info("creating temp ruleset ", "path", path, "name", name) - _, err := os.Stat(path) - if os.IsNotExist(err) { - return nil - } - if err != nil { - return err - } - tempRuleSet := engine.RuleSet{ - Name: name, - Description: "temp ruleset", - } - yamlData, err := yaml.Marshal(&tempRuleSet) - if err != nil { - return err - } - err = os.WriteFile(filepath.Join(path, "ruleset.yaml"), yamlData, os.ModePerm) - if err != nil { - return err - } - return nil -} - -func (a *analyzeCommand) createContainerNetwork() (string, error) { - networkName := fmt.Sprintf("network-%v", container.RandomName()) - args := []string{ - "network", - "create", - networkName, - } - - cmd := exec.Command(Settings.ContainerBinary, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return "", err - } - a.log.V(1).Info("created container network", "network", networkName) - // for cleanup - a.networkName = networkName - return networkName, nil -} - -// TODO: create for each source input once accepting multiple apps is completed -func (a *analyzeCommand) createContainerVolume() (string, error) { - volName := fmt.Sprintf("volume-%v", container.RandomName()) - input, err := filepath.Abs(a.input) - if err != nil { - return "", err - } - if a.isFileInput { - input = filepath.Dir(input) - } - if runtime.GOOS == "windows" { - // TODO(djzager): Thank ChatGPT - // Extract the volume name (e.g., "C:") - // Remove the volume name from the path to get the remaining part - // Convert backslashes to forward slashes - // Remove the colon from the volume name and convert to lowercase - volumeName := filepath.VolumeName(input) - remainingPath := input[len(volumeName):] - remainingPath = filepath.ToSlash(remainingPath) - driveLetter := strings.ToLower(strings.TrimSuffix(volumeName, ":")) - - // Construct the Linux-style path - input = fmt.Sprintf("/mnt/%s%s", driveLetter, remainingPath) - } - - args := []string{ - "volume", - "create", - "--opt", - "type=none", - "--opt", - fmt.Sprintf("device=%v", input), - "--opt", - "o=bind", - volName, - } - cmd := exec.Command(Settings.ContainerBinary, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { - return "", err - } - a.log.V(1).Info("created container volume", "volume", volName) - // for cleanup - a.volumeName = volName - return volName, nil -} - func (a *analyzeCommand) retryProviderContainer(ctx context.Context, networkName string, volName string, retry int) error { if retry == 0 { return fmt.Errorf("too many provider container retry attempts") @@ -1652,20 +1384,6 @@ func (a *analyzeCommand) getLabelSelector() string { return "" } -func isXMLFile(rule string) bool { - return path.Ext(rule) == ".xml" -} - -func loadEnvInsensitive(variableName string) string { - lowerValue := os.Getenv(strings.ToLower(variableName)) - upperValue := os.Getenv(strings.ToUpper(variableName)) - if lowerValue != "" { - return lowerValue - } else { - return upperValue - } -} - func (a *analyzeCommand) getXMLRulesVolumes(tempRuleDir string) (map[string]string, error) { rulesVolumes := make(map[string]string) mountTempDir := false diff --git a/cmd/cleanup.go b/cmd/cleanup.go index cdf3078c..e8e7b8f5 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -34,48 +34,48 @@ func (a *analyzeCommand) CleanAnalysisResources(ctx context.Context) error { return nil } -func (a *analyzeCommand) RmNetwork(ctx context.Context) error { - if a.networkName == "" { +func (c *AnalyzeCommandContext) RmNetwork(ctx context.Context) error { + if c.networkName == "" { return nil } cmd := exec.CommandContext( ctx, Settings.ContainerBinary, "network", - "rm", a.networkName) - a.log.V(1).Info("removing container network", - "network", a.networkName) + "rm", c.networkName) + c.log.V(1).Info("removing container network", + "network", c.networkName) return cmd.Run() } -func (a *analyzeCommand) RmVolumes(ctx context.Context) error { - if a.volumeName == "" { +func (c *AnalyzeCommandContext) RmVolumes(ctx context.Context) error { + if c.volumeName == "" { return nil } cmd := exec.CommandContext( ctx, Settings.ContainerBinary, "volume", - "rm", a.volumeName) - a.log.V(1).Info("removing created volume", - "volume", a.volumeName) + "rm", c.volumeName) + c.log.V(1).Info("removing created volume", + "volume", c.volumeName) return cmd.Run() } -func (a *analyzeCommand) RmProviderContainers(ctx context.Context) error { +func (c *AnalyzeCommandContext) RmProviderContainers(ctx context.Context) error { // if multiple provider containers, we need to remove the first created provider container last - for i := len(a.providerContainerNames) - 1; i >= 0; i-- { - con := a.providerContainerNames[i] + for i := len(c.providerContainerNames) - 1; i >= 0; i-- { + con := c.providerContainerNames[i] // because we are using the --rm option when we start the provider container, // it will immediately be removed after it stops cmd := exec.CommandContext( ctx, Settings.ContainerBinary, "stop", con) - a.log.V(1).Info("stopping provider container", "container", con) + c.log.V(1).Info("stopping provider container", "container", con) err := cmd.Run() if err != nil { - a.log.V(1).Error(err, "failed to stop container", + c.log.V(1).Error(err, "failed to stop container", "container", con) continue } diff --git a/cmd/command-context.go b/cmd/command-context.go new file mode 100644 index 00000000..91b505fd --- /dev/null +++ b/cmd/command-context.go @@ -0,0 +1,232 @@ +package cmd + +import ( + "fmt" + "github.com/devfile/alizer/pkg/apis/model" + "github.com/go-logr/logr" + "github.com/konveyor-ecosystem/kantra/pkg/container" + "github.com/konveyor/analyzer-lsp/engine" + "github.com/phayes/freeport" + "gopkg.in/yaml.v2" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +type AnalyzeCommandContext struct { + providersMap map[string]ProviderInit + + // 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 + needsBuiltin bool + // used for cleanup + networkName string + volumeName string + providerContainerNames []string + + // for containerless cmd + reqMap map[string]string + kantraDir string +} + +func (c *AnalyzeCommandContext) setProviders(providers []string, languages []model.Language, foundProviders []string) ([]string, error) { + if len(providers) > 0 { + for _, p := range providers { + foundProviders = append(foundProviders, p) + return foundProviders, nil + } + } + for _, l := range languages { + if l.CanBeComponent { + c.log.V(5).Info("Got language", "component language", l) + if l.Name == "C#" { + for _, item := range l.Frameworks { + supported, ok := DotnetFrameworks[item] + if ok { + if !supported { + err := fmt.Errorf("unsupported .NET Framework version") + c.log.Error(err, ".NET Framework version must be greater or equal 'v4.5'") + return foundProviders, err + } + return []string{dotnetFrameworkProvider}, nil + } + } + foundProviders = append(foundProviders, dotnetProvider) + continue + } + if l.Name == "JavaScript" { + for _, item := range l.Tools { + if item == "NodeJs" || item == "Node.js" || item == "nodejs" { + foundProviders = append(foundProviders, nodeJSProvider) + // only need one instance of provider + break + } + } + } else { + foundProviders = append(foundProviders, strings.ToLower(l.Name)) + } + } + } + return foundProviders, nil +} + +func (c *AnalyzeCommandContext) setProviderInitInfo(foundProviders []string) error { + for _, prov := range foundProviders { + port, err := freeport.GetFreePort() + if err != nil { + return err + } + switch prov { + case javaProvider: + c.providersMap[javaProvider] = ProviderInit{ + port: port, + image: Settings.JavaProviderImage, + provider: &JavaProvider{}, + } + case goProvider: + c.providersMap[goProvider] = ProviderInit{ + port: port, + image: Settings.GenericProviderImage, + provider: &GoProvider{}, + } + case pythonProvider: + c.providersMap[pythonProvider] = ProviderInit{ + port: port, + image: Settings.GenericProviderImage, + provider: &PythonProvider{}, + } + case nodeJSProvider: + c.providersMap[nodeJSProvider] = ProviderInit{ + port: port, + image: Settings.GenericProviderImage, + provider: &NodeJsProvider{}, + } + case dotnetProvider: + c.providersMap[dotnetProvider] = ProviderInit{ + port: port, + image: Settings.DotnetProviderImage, + provider: &DotNetProvider{}, + } + } + } + return nil +} + +func (c *AnalyzeCommandContext) handleDir(p string, tempDir string, basePath string) error { + newDir, err := filepath.Rel(basePath, p) + if err != nil { + return err + } + tempDir = filepath.Join(tempDir, newDir) + c.log.Info("creating nested tmp dir", "tempDir", tempDir, "newDir", newDir) + err = os.Mkdir(tempDir, 0777) + if err != nil { + return err + } + c.log.V(5).Info("create temp rule set for dir", "dir", tempDir) + err = c.createTempRuleSet(tempDir, filepath.Base(p)) + if err != nil { + c.log.V(1).Error(err, "failed to create temp ruleset", "path", tempDir) + return err + } + return err +} + +func (c *AnalyzeCommandContext) createTempRuleSet(path string, name string) error { + c.log.Info("creating temp ruleset ", "path", path, "name", name) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + tempRuleSet := engine.RuleSet{ + Name: name, + Description: "temp ruleset", + } + yamlData, err := yaml.Marshal(&tempRuleSet) + if err != nil { + return err + } + err = os.WriteFile(filepath.Join(path, "ruleset.yaml"), yamlData, os.ModePerm) + if err != nil { + return err + } + return nil +} + +func (c *AnalyzeCommandContext) createContainerNetwork() (string, error) { + networkName := fmt.Sprintf("network-%v", container.RandomName()) + args := []string{ + "network", + "create", + networkName, + } + + cmd := exec.Command(Settings.ContainerBinary, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return "", err + } + c.log.V(1).Info("created container network", "network", networkName) + // for cleanup + c.networkName = networkName + return networkName, nil +} + +// TODO: create for each source input once accepting multiple apps is completed +func (c *AnalyzeCommandContext) createContainerVolume(inputPath string) (string, error) { + volName := fmt.Sprintf("volume-%v", container.RandomName()) + input, err := filepath.Abs(inputPath) + if err != nil { + return "", err + } + if c.isFileInput { + input = filepath.Dir(input) + } + if runtime.GOOS == "windows" { + // TODO(djzager): Thank ChatGPT + // Extract the volume name (e.g., "C:") + // Remove the volume name from the path to get the remaining part + // Convert backslashes to forward slashes + // Remove the colon from the volume name and convert to lowercase + volumeName := filepath.VolumeName(input) + remainingPath := input[len(volumeName):] + remainingPath = filepath.ToSlash(remainingPath) + driveLetter := strings.ToLower(strings.TrimSuffix(volumeName, ":")) + + // Construct the Linux-style path + input = fmt.Sprintf("/mnt/%s%s", driveLetter, remainingPath) + } + + args := []string{ + "volume", + "create", + "--opt", + "type=none", + "--opt", + fmt.Sprintf("device=%v", input), + "--opt", + "o=bind", + volName, + } + cmd := exec.Command(Settings.ContainerBinary, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return "", err + } + c.log.V(1).Info("created container volume", "volume", volName) + // for cleanup + c.volumeName = volName + return volName, nil +} diff --git a/cmd/util.go b/cmd/util.go index 76050b91..e9642083 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -6,6 +6,8 @@ import ( "io" "io/fs" "os" + "path" + "path/filepath" "slices" "sort" "strings" @@ -32,6 +34,74 @@ const ( dependencyProviderPath = "dependencyProviderPath" ) +func copyFolderContents(src string, dst string) error { + err := os.MkdirAll(dst, os.ModePerm) + if err != nil { + return err + } + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + contents, err := source.Readdir(-1) + if err != nil { + return err + } + + for _, item := range contents { + sourcePath := filepath.Join(src, item.Name()) + destinationPath := filepath.Join(dst, item.Name()) + + if item.IsDir() { + // Recursively copy subdirectories + if err := copyFolderContents(sourcePath, destinationPath); err != nil { + return err + } + } else { + // Copy file + if err := CopyFileContents(sourcePath, destinationPath); err != nil { + return err + } + } + } + + return nil +} + +func CopyFileContents(src string, dst string) (err error) { + source, err := os.Open(src) + if err != nil { + return nil + } + defer source.Close() + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + _, err = io.Copy(destination, source) + if err != nil { + return err + } + return nil +} + +func LoadEnvInsensitive(variableName string) string { + lowerValue := os.Getenv(strings.ToLower(variableName)) + upperValue := os.Getenv(strings.ToUpper(variableName)) + if lowerValue != "" { + return lowerValue + } else { + return upperValue + } +} + +func isXMLFile(rule string) bool { + return path.Ext(rule) == ".xml" +} + func walkRuleSets(root string, label string, labelsSlice *[]string) fs.WalkDirFunc { return func(path string, d fs.DirEntry, err error) error { if !d.IsDir() {