Skip to content

Commit

Permalink
♻️ Refactor core module
Browse files Browse the repository at this point in the history
  • Loading branch information
tiulpin committed Feb 10, 2022
1 parent d2b6fbf commit ddeb2e7
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 133 deletions.
1 change: 0 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,5 @@ func NewInitCommand() *cobra.Command {
}
flags := cmd.Flags()
flags.StringVarP(&options.ProjectDir, "project-dir", "i", ".", "Root directory of the project to configure")
// TODO: the flag to set up supported CIs, e.g. --github tells to create .github/workflows/code_scanning.yml
return cmd
}
3 changes: 3 additions & 0 deletions cmd/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ func NewPullCommand() *cobra.Command {
Use: "pull",
Short: "Pull latest version of linter",
Long: `An alternative to docker pull.`,
PreRun: func(cmd *cobra.Command, args []string) {
core.CheckDockerHost()
},
Run: func(cmd *cobra.Command, args []string) {
if options.Linter == "" {
qodanaYaml := core.GetQodanaYaml(options.ProjectDir)
Expand Down
33 changes: 17 additions & 16 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Note that most options can be configured via qodana.yaml (https://www.jetbrains.
But you can always override qodana.yaml options with the following command-line options.
`,
PreRun: func(cmd *cobra.Command, args []string) {
core.EnsureDockerRunning()
core.CheckDockerHost()
},
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
Expand Down Expand Up @@ -99,35 +99,36 @@ But you can always override qodana.yaml options with the following command-line
}

flags := cmd.Flags()
flags.StringVarP(&options.AnalysisId, "analysis-id", "a", "", "Unique report identifier (GUID) to be used by Qodana Cloud")
flags.StringVarP(&options.Baseline, "baseline", "b", "", "Provide the path to an existing SARIF report to be used in the baseline state calculation")

flags.StringVarP(&options.Linter, "linter", "l", "", "Override linter to use")
flags.StringVarP(&options.ProjectDir, "project-dir", "i", ".", "Root directory of the inspected project")
flags.StringVarP(&options.ResultsDir, "results-dir", "o", "", "Override directory to save Qodana inspection results to (default <userCacheDir>/JetBrains/<linter>/results)")
flags.StringVar(&options.CacheDir, "cache-dir", "", "Override cache directory (default <userCacheDir>/JetBrains/<linter>/cache)")
flags.StringVarP(&options.SourceDirectory, "source-directory", "d", "", "Directory inside the project-dir directory must be inspected. If not specified, the whole project is inspected")
flags.StringArrayVarP(&options.Env, "env", "e", []string{}, "Define additional environment variables for the Qodana container (you can use the flag multiple times). CLI is not reading full host environment variables and does not pass it to the Qodana container for security reasons")
flags.StringArrayVarP(&options.Volumes, "volume", "v", []string{}, "Define additional volumes for the Qodana container (you can use the flag multiple times)")
flags.StringVarP(&options.User, "user", "u", "", "User to run Qodana container as (default: the current user)")

flags.StringVarP(&options.ProjectDir, "project-dir", "i", ".", "Root directory of the inspected project")
flags.StringVarP(&options.Linter, "linter", "l", "", "Override linter to use")
flags.StringVarP(&options.ResultsDir, "results-dir", "o", "", "Override directory to save Qodana inspection results to (default <userCacheDir>/JetBrains/<linter>/results)")
flags.StringVarP(&options.ProfileName, "profile-name", "n", "", "Profile name defined in the project")
flags.StringVarP(&options.ProfilePath, "profile-path", "p", "", "Path to the profile file")
flags.BoolVarP(&options.SaveReport, "save-report", "s", true, "Generate HTML report")
flags.StringVarP(&options.Token, "token", "t", "", "Qodana Cloud token")
flags.BoolVar(&options.SkipPull, "skip-pull", false, "Skip pulling the latest Qodana container")
flags.BoolVar(&options.PrintProblems, "print-problems", false, "Print all found problems by Qodana in the CLI output")
flags.BoolVarP(&options.ShowReport, "show-report", "w", false, "Serve HTML report on port")
flags.BoolVar(&options.SkipPull, "skip-pull", false, "Skip pulling the latest Qodana container")
flags.IntVar(&options.Port, "port", 8080, "Port to serve the report on")

flags.StringVarP(&options.AnalysisId, "analysis-id", "a", "", "Unique report identifier (GUID) to be used by Qodana Cloud")
flags.StringVarP(&options.Baseline, "baseline", "b", "", "Provide the path to an existing SARIF report to be used in the baseline state calculation")
flags.BoolVar(&options.BaselineIncludeAbsent, "baseline-include-absent", false, "Include in the output report the results from the baseline run that are absent in the current run")
flags.BoolVarP(&options.Changes, "changes", "c", false, "Override the docker image to be used for the analysis")
flags.StringVar(&options.FailThreshold, "fail-threshold", "", "Set the number of problems that will serve as a quality gate. If this number is reached, the inspection run is terminated with a non-zero exit code")
flags.BoolVar(&options.DisableSanity, "disable-sanity", false, "Skip running the inspections configured by the sanity profile")
flags.StringVarP(&options.SourceDirectory, "source-directory", "d", "", "Directory inside the project-dir directory must be inspected. If not specified, the whole project is inspected")
flags.StringVarP(&options.ProfileName, "profile-name", "n", "", "Profile name defined in the project")
flags.StringVarP(&options.ProfilePath, "profile-path", "p", "", "Path to the profile file")
flags.BoolVar(&options.RunPromo, "run-promo", false, "Set to true to have the application run the inspections configured by the promo profile; set to false otherwise. By default, a promo run is enabled if the application is executed with the default profile and is disabled otherwise")
flags.StringVar(&options.Script, "script", "default", "Override the run scenario")
flags.StringVar(&options.StubProfile, "stub-profile", "", "Absolute path to the fallback profile file. This option is applied in case the profile was not specified using any available options")
flags.BoolVar(&options.BaselineIncludeAbsent, "baseline-include-absent", false, "Include in the output report the results from the baseline run that are absent in the current run")

flags.StringVar(&options.Property, "property", "", "Set a JVM property to be used while running Qodana using the --property=property.name=value1,value2,...,valueN notation")
flags.IntVar(&options.Port, "port", 8080, "Port to serve the report on")
flags.StringVar(&options.Script, "script", "default", "Override the run scenario")
flags.BoolVarP(&options.SaveReport, "save-report", "s", true, "Generate HTML report")
flags.BoolVar(&options.SendReport, "send-report", false, "Send the inspection report to Qodana Cloud, requires the '--token' option to be specified")
flags.StringVarP(&options.Token, "token", "t", "", "Qodana Cloud token")

return cmd
}
138 changes: 24 additions & 114 deletions core/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,15 @@ import (
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"

"github.com/erikgeiser/promptkit/selection"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
)

type QodanaOptions struct { // TODO: get available options from the image / have another scheme
type QodanaOptions struct {
ResultsDir string
CacheDir string
ProjectDir string
Expand Down Expand Up @@ -70,18 +65,24 @@ type QodanaOptions struct { // TODO: get available options from the image / have
SkipPull bool
}

//goland:noinspection GoUnusedGlobalVariable
var (
Version = "0.7.2"
DoNotTrack = false
Interrupted = false
scanStages = []string{
UnofficialLinter = false
Version = "0.7.3"
DoNotTrack = false
Interrupted = false
scanStages = []string{
"Preparing Qodana Docker images",
"Starting the analysis engine",
"Opening the project",
"Configuring the project",
"Analyzing the project",
"Preparing the report",
}
notSupportedLinters = []string{
"jetbrains/qodana-license-audit",
"jetbrains/qodana-clone-finder",
}
releaseUrl = "https://api.github.com/repos/JetBrains/qodana-cli/releases/latest"
)

Expand Down Expand Up @@ -140,6 +141,7 @@ func CheckForUpdates(currentVersion string) {
}()
}

// GetLinter gets linter for the given path
func GetLinter(path string) string {
var linters []string
var linter string
Expand Down Expand Up @@ -178,22 +180,27 @@ func CheckLinter(image string) {
}
}

// GetLinterSystemDir returns path to <userCacheDir>/JetBrains/<linter>/<project-hash>/
// getProjectId returns the project id for internal CLI usage from the given path.
func getProjectId(project string) string {
projectAbs, _ := filepath.Abs(project)
sha256sum := sha256.Sum256([]byte(projectAbs))
return hex.EncodeToString(sha256sum[:])
}

// GetLinterSystemDir returns path to <userCacheDir>/JetBrains/<linter>/<project-id>/.
func GetLinterSystemDir(project string, linter string) string {
userCacheDir, _ := os.UserCacheDir()
linterDirName := strings.Replace(strings.Replace(linter, ":", "-", -1), "/", "-", -1)
projectAbs, _ := filepath.Abs(project)
sha256sum := sha256.Sum256([]byte(projectAbs))

return filepath.Join(
userCacheDir,
"JetBrains",
linterDirName,
hex.EncodeToString(sha256sum[:]),
getProjectId(project),
)
}

// PrepareHost cleans up report folder, gets the current user, creates the necessary folders for the analysis
// PrepareHost cleans up report folder, gets the current user, creates the necessary folders for the analysis.
func PrepareHost(opts *QodanaOptions) {
linterHome := GetLinterSystemDir(opts.ProjectDir, opts.Linter)
if opts.ResultsDir == "" {
Expand Down Expand Up @@ -233,70 +240,9 @@ func ShowReport(path string, port int) { // TODO: Open report from Cloud
)
}

// openReport serves the report on the given port and opens the browser.
func openReport(path string, port int) {
url := fmt.Sprintf("http://localhost:%d", port)
go func() {
resp, err := http.Get(url)
if err == nil && resp.StatusCode == 200 {
err := openBrowser(url)
if err != nil {
return
}
}
}()
http.Handle("/", noCache(http.FileServer(http.Dir(path))))
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
if err != nil {
WarningMessage("Problem serving report, %s\n", err.Error())
return
}
_, _ = fmt.Scan()
}

// openBrowser opens the default browser to the given url
func openBrowser(url string) error {
var cmd string
var args []string

switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}

// OpenDir opens directory in the default file manager
func OpenDir(path string) error {
var cmd string
var args []string

switch runtime.GOOS {
case "windows":
cmd = "explorer"
args = []string{"/select"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, path)
return exec.Command(cmd, args...).Start()
}

// RunLinter runs the linter with the given options.
func RunLinter(ctx context.Context, options *QodanaOptions) int {
docker, err := client.NewClientWithOpts()
checkDockerMemory(docker)
if err != nil {
log.Fatal("couldn't instantiate docker client", err)
}
docker := getDockerClient()
for i, stage := range scanStages {
scanStages[i] = PrimaryBold("[%d/%d] ", i+1, len(scanStages)+1) + Primary(stage)
}
Expand All @@ -310,12 +256,7 @@ func RunLinter(ctx context.Context, options *QodanaOptions) int {
updateText(progress, scanStages[1])
runContainer(ctx, docker, dockerConfig)

reader, _ := docker.ContainerLogs(context.Background(), dockerConfig.Name, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Timestamps: false,
})
reader, _ := docker.ContainerLogs(ctx, dockerConfig.Name, dockerLogsOptions)
defer func(reader io.ReadCloser) {
err := reader.Close()
if err != nil {
Expand Down Expand Up @@ -351,34 +292,3 @@ func RunLinter(ctx context.Context, options *QodanaOptions) int {
}
return int(exitCode)
}

// noCache handles serving the static files with no cache headers.
func noCache(h http.Handler) http.Handler {
etagHeaders := []string{
"ETag",
"If-Modified-Since",
"If-Match",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
}
epoch := time.Unix(0, 0).Format(time.RFC1123)
noCacheHeaders := map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
fn := func(w http.ResponseWriter, r *http.Request) {
for _, v := range etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
2 changes: 0 additions & 2 deletions core/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import (
log "github.com/sirupsen/logrus"
)

// TODO: unify logging/error exiting messages across the codebase

// Info Two newlines at the start are important to lay the output nicely in CLI.
var Info = fmt.Sprintf(`
%s (v%s)
Expand Down
97 changes: 97 additions & 0 deletions core/system.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package core

import (
"fmt"
"net/http"
"os/exec"
"runtime"
"time"
)

// openReport serves the report on the given port and opens the browser.
func openReport(path string, port int) {
url := fmt.Sprintf("http://localhost:%d", port)
go func() {
resp, err := http.Get(url)
if err == nil && resp.StatusCode == 200 {
err := openBrowser(url)
if err != nil {
return
}
}
}()
http.Handle("/", noCache(http.FileServer(http.Dir(path))))
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
if err != nil {
WarningMessage("Problem serving report, %s\n", err.Error())
return
}
_, _ = fmt.Scan()
}

// openBrowser opens the default browser to the given url
func openBrowser(url string) error {
var cmd string
var args []string

switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}

// OpenDir opens directory in the default file manager
func OpenDir(path string) error {
var cmd string
var args []string

switch runtime.GOOS {
case "windows":
cmd = "explorer"
args = []string{"/select"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, path)
return exec.Command(cmd, args...).Start()
}

// noCache handles serving the static files with no cache headers.
func noCache(h http.Handler) http.Handler {
etagHeaders := []string{
"ETag",
"If-Modified-Since",
"If-Match",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
}
epoch := time.Unix(0, 0).Format(time.RFC1123)
noCacheHeaders := map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
fn := func(w http.ResponseWriter, r *http.Request) {
for _, v := range etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

0 comments on commit ddeb2e7

Please sign in to comment.