From 08f3d107049e9c51a57462ea969c94ffe333b0c5 Mon Sep 17 00:00:00 2001 From: David Zager Date: Thu, 13 Jun 2024 12:01:32 -0400 Subject: [PATCH] :sparkles: kantra support .net provider on windows Signed-off-by: David Zager --- Dockerfile.windows | 66 ++++++++ cmd/analyze.go | 319 +++++++++++++++++++++++++++++++++---- cmd/settings.go | 1 + pkg/container/container.go | 12 ++ 4 files changed, 365 insertions(+), 33 deletions(-) create mode 100644 Dockerfile.windows diff --git a/Dockerfile.windows b/Dockerfile.windows new file mode 100644 index 0000000..f3df30a --- /dev/null +++ b/Dockerfile.windows @@ -0,0 +1,66 @@ +ARG VERSION=latest + +# FROM quay.io/konveyor/static-report:${VERSION} as static-report + +FROM mcr.microsoft.com/windows/servercore:ltsc2022 AS rulesets +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +ENV GIT_VERSION 2.45.2 +ENV GIT_SHA256 7ed2a3ce5bbbf8eea976488de5416894ca3e6a0347cee195a7d768ac146d5290 + +RUN $url = ('https://github.com/git-for-windows/git/releases/download/v{0}.windows.1/MinGit-{0}-64-bit.zip' -f $env:GIT_VERSION); \ + Write-Host ('Downloading {0} ...' -f $url); \ + Invoke-WebRequest -Uri $url -OutFile 'git.zip'; \ + \ + Write-Host ('Verifying sha256 ({0}) ...' -f $env:GIT_SHA256); \ + if ((Get-FileHash git.zip -Algorithm sha256).Hash -ne $env:GIT_SHA256) { throw 'SHA256 mismatch' }; \ + \ + Write-Host 'Expanding ...'; \ + Expand-Archive git.zip -DestinationPath C:\git; \ + \ + Write-Host 'Removing ...'; \ + Remove-Item git.zip -Force; \ + \ + Write-Host 'Verifying ("git --version") ...'; \ + C:\git\cmd\git.exe --version; \ + \ + Write-Host 'Complete.'; + +ARG RULESETS_REF=main +RUN C:\git\cmd\git.exe clone https://github.com/konveyor/rulesets -b $env:RULESETS_REF C:\rulesets + +FROM golang:1.21-windowsservercore-ltsc2022 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY main.go main.go +COPY cmd/ cmd/ +COPY pkg/ pkg/ + +# Build +ARG VERSION=latest +ARG BUILD_COMMIT +RUN go build -o kantra.exe main.go + +FROM quay.io/konveyor/analyzer-lsp:${VERSION}-windowsservercore-ltsc2022 + +# Set the working directory inside the container +WORKDIR C:/app + +RUN md C:\opt\rulesets\input C:\opt\rulesets\convert C:\opt\openrewrite C:\opt\input\rules\custom C:\opt\output C:\opt\xmlrules C:\opt\shimoutput C:\tmp\source-app C:\tmp\source-app\input + +# Copy the executable from the builder stage +COPY --from=builder /workspace/kantra.exe . +COPY --from=rulesets /rulesets/default/generated/dotnet8 /opt/rulesets +#COPY --from=static-report /usr/bin/js-bundle-generator . +#COPY --from=static-report /usr/local/static-report . + +# Command to run the executable +ENTRYPOINT ["kantra.exe"] diff --git a/cmd/analyze.go b/cmd/analyze.go index c93576d..a41d4ad 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -15,6 +15,7 @@ import ( "runtime" "path/filepath" + "slices" "sort" "strings" @@ -32,7 +33,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/exp/maps" - "golang.org/x/exp/slices" ) var ( @@ -48,13 +48,34 @@ var ( AnalysisOutputMountPath = path.Join(OutputPath, "output.yaml") DepsOutputMountPath = path.Join(OutputPath, "dependencies.yaml") ProviderSettingsMountPath = path.Join(ConfigMountPath, "settings.json") + DotnetFrameworks = map[string]bool{ + "v1.0": false, + "v1.1": false, + "v2.0": false, + "v3.0": false, + "v3.5": false, + "v4": false, + "v4.5": true, + "v4.5.1": true, + "v4.5.2": true, + "v4.6": true, + "v4.6.1": true, + "v4.6.2": true, + "v4.7": true, + "v4.7.1": true, + "v4.7.2": true, + "v4.8": true, + "v4.8.1": true, + } ) const ( - javaProvider = "java" - goProvider = "go" - pythonProvider = "python" - nodeJSProvider = "javascript" + javaProvider = "java" + goProvider = "go" + pythonProvider = "python" + nodeJSProvider = "javascript" + dotnetProvider = "dotnet" + dotnetFrameworkProvider = "dotnetFramework" ) // provider config options @@ -195,6 +216,9 @@ func NewAnalyzeCmd(log logr.Logger) *cobra.Command { return err } } + if len(foundProviders) == 1 && foundProviders[0] == dotnetFrameworkProvider { + return analyzeCmd.analyzeDotnetFramework(cmd.Context()) + } err = analyzeCmd.setProviderInitInfo(foundProviders) if err != nil { log.Error(err, "failed to set provider init info") @@ -452,33 +476,25 @@ func (a *analyzeCommand) setProviders(components []model.Component, foundProvide for _, c := range components { a.log.V(5).Info("Got component", "component language", c.Languages, "path", c.Path) for _, l := range c.Languages { - // if l.Name == "C#" { - // foundDotNetProv, err := setDotNetProvider(l.Frameworks) - // if err != nil { - // return nil, err - // } - // foundProviders = append(foundProviders, foundDotNetProv) - // } else { - foundProviders = append(foundProviders, strings.ToLower(l.Name)) - //} + 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, strings.ToLower(l.Name)) + } } } return foundProviders, nil } -// func setDotNetProvider(frameworks []string) (string, error) { -// if len(frameworks) > 0 { -// for _, f := range frameworks { -// if f == "v4.5" || f == "v4.6" || f == "v4.7" || f == "v4.8" { -// return dotNetFrameworkProvider, nil -// } else { -// return dotNetProvider, nil -// } -// } -// } -// return "", fmt.Errorf("unable to set dotnet provider") -// } - func (a *analyzeCommand) setProviderInitInfo(foundProviders []string) error { for _, prov := range foundProviders { port, err := freeport.GetFreePort() @@ -506,12 +522,11 @@ func (a *analyzeCommand) setProviderInitInfo(foundProviders []string) error { port: port, image: Settings.GenericProviderImage, } - // TODO - // case dotNetProvider, dotNetFrameworkProvider: - // providers[nodeJSProvider] = ProviderInit{ - // port: port, - // image: "", - // } + case dotnetProvider: + a.providersMap[dotnetProvider] = ProviderInit{ + port: port, + image: Settings.DotnetProviderImage, + } } } return nil @@ -523,6 +538,8 @@ func (a *analyzeCommand) validateProviders(providers []string) error { pythonProvider, goProvider, nodeJSProvider, + dotnetProvider, + dotnetFrameworkProvider, } for _, prov := range providers { //validate other providers @@ -825,6 +842,20 @@ func (a *analyzeCommand) getConfigVolumes() (map[string]string, error) { }, } + dotnetConfig := provider.Config{ + Name: dotnetProvider, + Address: fmt.Sprintf("0.0.0.0:%v", a.providersMap[dotnetProvider].port), + InitConfig: []provider.InitConfig{ + { + Location: SourceMountPath, + AnalysisMode: provider.SourceOnlyAnalysisMode, + ProviderSpecificConfig: map[string]interface{}{ + provider.LspServerPathConfigKey: "/opt/app-root/.dotnet/tools/csharp-ls", + }, + }, + }, + } + provConfig := []provider.Config{ { Name: "builtin", @@ -853,6 +884,8 @@ func (a *analyzeCommand) getConfigVolumes() (map[string]string, error) { nodeJSConfig.InitConfig[0].ProviderSpecificConfig["dependencyFolders"] = dependencyFolders } provConfig = append(provConfig, nodeJSConfig) + case dotnetProvider: + provConfig = append(provConfig, dotnetConfig) } } for prov, _ := range a.providersMap { @@ -1875,6 +1908,9 @@ func (a *analyzeCommand) CleanAnalysisResources(ctx context.Context) error { } func (a *analyzeCommand) RmNetwork(ctx context.Context) error { + if a.networkName == "" { + return nil + } cmd := exec.CommandContext( ctx, Settings.PodmanBinary, @@ -1886,6 +1922,9 @@ func (a *analyzeCommand) RmNetwork(ctx context.Context) error { } func (a *analyzeCommand) RmVolumes(ctx context.Context) error { + if a.volumeName == "" { + return nil + } cmd := exec.CommandContext( ctx, Settings.PodmanBinary, @@ -1944,3 +1983,217 @@ func (a *analyzeCommand) getProviderLogs(ctx context.Context) error { return nil } + +func (a *analyzeCommand) analyzeDotnetFramework(ctx context.Context) error { + if runtime.GOOS != "windows" { + err := fmt.Errorf("Unsupported OS") + a.log.Error(err, "Analysis of .NET Framework projects is only supported on Windows") + return err + } + + // TODO(djzager): uncomment when provider handles mode correctly + //if a.mode == string(provider.FullAnalysisMode) { + // a.log.V(1).Info("Only source mode analysis is supported") + // a.mode = string(provider.SourceOnlyAnalysisMode) + //} + + var err error + + // Create network + networkName := container.RandomName() + cmd := exec.Command(Settings.PodmanBinary, []string{"network", "create", "-d", "nat", networkName}...) + 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) + a.networkName = networkName + // end create network + + // Create volume + // opts aren't supported on Windows + // containerVolName, err := a.createContainerVolume() + // if err != nil { + // a.log.Error(err, "failed to create container volume") + // return err + // } + + // Run provider + //foundProviders := []string{dotnetFrameworkProvider} + //providerPorts, err := a.RunProviders(ctx, networkName, containerVolName, foundProviders, 5) + //if err != nil { + // a.log.Error(err, "failed to run provider") + // return err + //} + input, err := filepath.Abs(a.input) + if err != nil { + return err + } + port, err := freeport.GetFreePort() + if err != nil { + return err + } + a.log.V(1).Info("Starting dotnet-external-provider") + providerContainer := container.NewContainer() + err = providerContainer.Run( + ctx, + container.WithImage(Settings.DotnetProviderImage), + container.WithLog(a.log.V(1)), + container.WithVolumes(map[string]string{ + input: "C:" + filepath.FromSlash(SourceMountPath), + }), + container.WithContainerToolBin(Settings.PodmanBinary), + container.WithEntrypointArgs([]string{fmt.Sprintf("--port=%v", port)}...), + container.WithDetachedMode(true), + container.WithCleanup(a.cleanup), + container.WithNetwork(networkName), + ) + if err != nil { + return err + } + a.providerContainerNames = append(a.providerContainerNames, providerContainer.Name) + a.log.V(1).Info("Provider started") + // end run provider + + // Run analysis + // err = a.RunAnalysis(ctx, "", containerVolName, foundProviders, providerPorts) + // if err != nil { + // a.log.Error(err, "failed to run analysis") + // return err + // } + tempDir, err := os.MkdirTemp("", "analyze-config-") + if err != nil { + a.log.V(1).Error(err, "failed creating temp dir", "dir", tempDir) + return err + } + a.log.V(1).Info("created directory for provider settings", "dir", tempDir) + a.tempDirs = append(a.tempDirs, tempDir) + + // Set the IP!!! + provConfig := []provider.Config{ + { + Name: "builtin", + InitConfig: []provider.InitConfig{ + { + Location: "C:" + filepath.FromSlash(SourceMountPath), + AnalysisMode: provider.AnalysisMode(a.mode), + }, + }, + }, + { + Name: dotnetProvider, + Address: fmt.Sprintf("%v:%v", providerContainer.Name, port), + InitConfig: []provider.InitConfig{ + { + Location: "C:" + filepath.FromSlash(SourceMountPath), + AnalysisMode: provider.AnalysisMode(a.mode), + ProviderSpecificConfig: map[string]interface{}{ + provider.LspServerPathConfigKey: "C:/Users/ContainerAdministrator/.dotnet/tools/csharp-ls.exe", + }, + }, + }, + }, + } + + jsonData, err := json.MarshalIndent(&provConfig, "", " ") + if err != nil { + a.log.V(1).Error(err, "failed to marshal provider config") + return err + } + err = os.WriteFile(filepath.Join(tempDir, "settings.json"), jsonData, os.ModePerm) + if err != nil { + a.log.V(1).Error(err, + "failed to write provider config", "dir", tempDir, "file", "settings.json") + return err + } + + volumes := map[string]string{ + tempDir: "C:" + filepath.FromSlash(ConfigMountPath), + input: "C:" + filepath.FromSlash(SourceMountPath), + a.output: "C:" + filepath.FromSlash(OutputPath), + } + + args := []string{ + fmt.Sprintf("--provider-settings=%s", "C:"+filepath.FromSlash(ProviderSettingsMountPath)), + fmt.Sprintf("--output-file=%s", "C:"+filepath.FromSlash(AnalysisOutputMountPath)), + fmt.Sprintf("--context-lines=%d", a.contextLines), + } + + if a.enableDefaultRulesets { + args = append(args, fmt.Sprintf("--rules=%s/", "C:"+filepath.FromSlash(RulesetPath))) + } + + if len(a.rules) > 0 { + for index, rule := range a.rules { + volumes[rule] = fmt.Sprintf("C:%v-%d", filepath.FromSlash(CustomRulePath), index) + } + args = append(args, fmt.Sprintf("--rules=%s/", CustomRulePath)) + } + + if a.jaegerEndpoint != "" { + args = append(args, "--enable-jaeger") + args = append(args, "--jaeger-endpoint") + args = append(args, a.jaegerEndpoint) + } + + if a.logLevel != nil { + args = append(args, fmt.Sprintf("--verbose=%d", *a.logLevel)) + } + labelSelector := a.getLabelSelector() + if labelSelector != "" { + args = append(args, fmt.Sprintf("--label-selector=%s", labelSelector)) + } + + analysisLogFilePath := filepath.Join(a.output, "analysis.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() + + a.log.Info("running source code analysis", "log", analysisLogFilePath, + "input", a.input, "output", a.output, "args", strings.Join(args, " "), "volumes", volumes) + a.log.Info("generating analysis log in file", "file", analysisLogFilePath) + + c := container.NewContainer() + err = c.Run( + ctx, + container.WithImage(Settings.RunnerImage), + container.WithLog(a.log.V(1)), + container.WithVolumes(volumes), + container.WithStdout(analysisLog), + container.WithStderr(analysisLog), + container.WithEntrypointArgs(args...), + container.WithEntrypointBin(`C:\app\konveyor-analyzer.exe`), + container.WithNetwork(networkName), + container.WithContainerToolBin(Settings.PodmanBinary), + container.WithCleanup(a.cleanup), + ) + if err != nil { + return err + } + err = a.getProviderLogs(ctx) + if err != nil { + a.log.Error(err, "failed to get provider container logs") + } + // end run analysis + + // Create json output + err = a.CreateJSONOutput() + if err != nil { + a.log.Error(err, "failed to create json output file") + return err + } + + // Generate Static Report + err = a.GenerateStaticReport(ctx) + if err != nil { + a.log.Error(err, "failed to generate static report") + return err + } + + return nil +} diff --git a/cmd/settings.go b/cmd/settings.go index f563f4c..de70bb9 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -29,6 +29,7 @@ type Config struct { RunLocal bool `env:"RUN_LOCAL"` JavaProviderImage string `env:"JAVA_PROVIDER_IMG" default:"quay.io/konveyor/java-external-provider:latest"` GenericProviderImage string `env:"GENERIC_PROVIDER_IMG" default:"quay.io/konveyor/generic-external-provider:latest"` + DotnetProviderImage string `env:"DOTNET_PROVIDER_IMG" default:"quay.io/konveyor/dotnet-external-provider:latest"` } func (c *Config) Load() error { diff --git a/pkg/container/container.go b/pkg/container/container.go index d9effb3..ba0de8e 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -23,6 +23,7 @@ type container struct { Name string image string NetworkName string + IPv4 string entrypointBin string entrypointArgs []string workdir string @@ -58,6 +59,12 @@ func WithNetwork(w string) Option { } } +func WithIPv4(ip string) Option { + return func(c *container) { + c.IPv4 = ip + } +} + func WithEntrypointBin(b string) Option { return func(c *container) { c.entrypointBin = b @@ -189,6 +196,10 @@ func (c *container) Run(ctx context.Context, opts ...Option) error { args = append(args, "--network") args = append(args, c.NetworkName) } + if c.IPv4 != "" { + args = append(args, "--ip") + args = append(args, c.IPv4) + } if c.entrypointBin != "" { args = append(args, "--entrypoint") args = append(args, c.entrypointBin) @@ -237,6 +248,7 @@ func (c *container) Run(ctx context.Context, opts ...Option) error { } c.log.Info("executing command", "container tool", c.containerToolBin, "cmd", c.entrypointBin, "args", strings.Join(args, " ")) + c.log.Info(cmd.String()) err = cmd.Run() if err != nil { c.log.Error(err, "container run error")