initial working plugin (happy path)
parent
921d0408f8
commit
6e5243f405
13
README.md
13
README.md
|
@ -1,3 +1,12 @@
|
|||
# plugin-sonar
|
||||
# Woodpecker plugin for integration with SonarQube
|
||||
|
||||
A Woodpecker-Ci plugin integrating sonar-scanner
|
||||
## Meta
|
||||
|
||||
This is one of my first go projects, it was created to facilitate the use of [Woodpecker-CI](https://woodpecker-ci.org) in combination with SonarQube.
|
||||
|
||||
Since I am using the [mc1arke/sonarqube-community-branch-plugin](https://github.com/mc1arke/sonarqube-community-branch-plugin) plugin for SonarQube, I needed a plugin capable of properly setting the scanner parameters to allow for PR (not implemented in any plugins I found) and branch scanning (implemented in aosapps/drone-sonar).
|
||||
|
||||
## Inspiration / acknowledgements
|
||||
|
||||
This plugin was inspired by the works of [aosapps/drone-sonar](https://github.com/aosapps/drone-sonar-plugin)
|
||||
and [diegopereiraeng/harness-cie-sonarqube-scanner](https://github.com/diegopereiraeng/harness-cie-sonarqube-scanner).
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
module git.kle.li/gapodo/woodpecker-plugin-sonar
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
git.kle.li/gapodo/woodpecker-plugin-lib v0.0.4
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/urfave/cli/v2 v2.20.3
|
||||
go.uber.org/zap v1.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/woodpecker-ci/woodpecker v0.15.5 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,40 @@
|
|||
git.kle.li/gapodo/woodpecker-plugin-lib v0.0.4 h1:+N8ZKk7NkpnCb3/nNTJwM8rjGlK1Wzi4uLzN7HY+wZM=
|
||||
git.kle.li/gapodo/woodpecker-plugin-lib v0.0.4/go.mod h1:HcNuDYsML3ZwNi/pYrgpzwEqkV/e+sC2eaPpaLmmBLQ=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
|
||||
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/woodpecker-ci/woodpecker v0.15.5 h1:8l6p1qhwUpjNXQNCkEpH6lQf0npfips4I8xYX9dBC4k=
|
||||
github.com/woodpecker-ci/woodpecker v0.15.5/go.mod h1:Po9JhNe1H/AInF5eeqOAfAsXMfdmfR8E7ghUxM0sDEY=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,252 @@
|
|||
// Copyright 2021 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
plugLib "git.kle.li/gapodo/woodpecker-plugin-lib/urfave"
|
||||
"git.kle.li/gapodo/woodpecker-plugin-sonar/pkg/plugin"
|
||||
"git.kle.li/gapodo/woodpecker-plugin-sonar/pkg/sonar"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var appVersion = "v0.0.1"
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "Woodpecker-SonarQube-Plugin",
|
||||
Usage: "Trigger SonarQube scan from a Woodpecker-CI pipeline",
|
||||
Version: appVersion,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Michael Amann [@gapodo:kle.li]",
|
||||
Email: "gapodo@geekvoid.net",
|
||||
},
|
||||
},
|
||||
Copyright: "(c) 2022 Michael Amann",
|
||||
Before: func(ctx *cli.Context) error {
|
||||
fmt.Fprintf(ctx.App.Writer, "Woodpecker-CI SonarQube plugin\n")
|
||||
return nil
|
||||
},
|
||||
After: func(ctx *cli.Context) error {
|
||||
fmt.Fprintf(ctx.App.Writer, "Exiting Woodpecker-CI SonarQube plugin\n")
|
||||
return nil
|
||||
},
|
||||
Action: run,
|
||||
Flags: Flags(),
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func Flags() []cli.Flag {
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "logLevel",
|
||||
Usage: "plugin log level (debug, info, warn, error, dpanic, panic, and fatal)",
|
||||
Value: "info",
|
||||
EnvVars: []string{"PLUGIN_LOG_LEVEL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Usage: "SonarQube project key",
|
||||
EnvVars: []string{"PLUGIN_SONAR_KEY"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "SonarQube project name",
|
||||
EnvVars: []string{"PLUGIN_SONAR_NAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "host-url",
|
||||
Usage: "SonarQube host url",
|
||||
EnvVars: []string{"PLUGIN_SONAR_URL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "SonarQube analysis token",
|
||||
EnvVars: []string{"PLUGIN_SONAR_TOKEN"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ver",
|
||||
Usage: "Project version",
|
||||
EnvVars: []string{"PLUGIN_SONAR_VERSION"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "http-timeout",
|
||||
Usage: "Web request timeout",
|
||||
Value: 300,
|
||||
EnvVars: []string{"PLUGIN_TIMEOUT"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sources",
|
||||
Usage: "analysis sources",
|
||||
Value: ".",
|
||||
EnvVars: []string{"PLUGIN_SOURCES"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "inclusions",
|
||||
Usage: "code inclusions",
|
||||
EnvVars: []string{"PLUGIN_INCLUSIONS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "exclusions",
|
||||
Usage: "code exclusions",
|
||||
EnvVars: []string{"PLUGIN_EXCLUSIONS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sonar-log-Level",
|
||||
Usage: "sonar-scanner log level (as per sonar-scanner)",
|
||||
Value: "INFO",
|
||||
EnvVars: []string{"PLUGIN_SONAR_LOG_LEVEL"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "showProfiling",
|
||||
Usage: "showProfiling during analysis",
|
||||
Value: false,
|
||||
EnvVars: []string{"PLUGIN_SHOWPROFILING"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "branchAnalysis",
|
||||
Usage: "execute branchAnalysis (true, auto, false)",
|
||||
EnvVars: []string{"PLUGIN_BRANCHANALYSIS"},
|
||||
Value: "false",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "usingProperties",
|
||||
Usage: "using sonar-project.properties",
|
||||
EnvVars: []string{"PLUGIN_USINGPROPERTIES"},
|
||||
Value: "false",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "binaries",
|
||||
Usage: "Java Binaries",
|
||||
EnvVars: []string{"PLUGIN_BINARIES"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "quality-target",
|
||||
Usage: "Quality Gate target",
|
||||
EnvVars: []string{"PLUGIN_QUALITYGATE"},
|
||||
Value: "OK",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quality-gate-enabled",
|
||||
Usage: "true or false - stop pipeline if sonar quality gate conditions are not met",
|
||||
Value: true,
|
||||
EnvVars: []string{"PLUGIN_SONAR_QUALITY_ENABLED"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "quality-gate-timeout",
|
||||
Usage: "number in seconds for timeout",
|
||||
Value: 300,
|
||||
EnvVars: []string{"PLUGIN_SONAR_QUALITY_GATE_TIMEOUT"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "artifact-file",
|
||||
Usage: "Artifact file location that will be generated by the plugin. This file will include information of docker images that are uploaded by the plugin.",
|
||||
Value: "artifact.json",
|
||||
EnvVars: []string{"PLUGIN_ARTIFACT_FILE"},
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, plugLib.BuildFlags()...)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func run(ctx *cli.Context) error {
|
||||
|
||||
slog := setupSugaredLogger(ctx.String("logLevel"))
|
||||
|
||||
slog.Infow("SonarQube plugin for Woodpecker", "version", appVersion)
|
||||
|
||||
slog.Infow("Parsing values from env")
|
||||
plugin := pluginFromContext(ctx, slog)
|
||||
slog.Infow("Starting scanner")
|
||||
plugin.Exec()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginFromContext(ctx *cli.Context, logger *zap.SugaredLogger) plugin.Plugin {
|
||||
config := sonar.Config{
|
||||
Key: ctx.String("key"),
|
||||
Name: ctx.String("name"),
|
||||
HostURL: ctx.String("host-url"),
|
||||
Token: ctx.String("token"),
|
||||
Version: ctx.String("ver"),
|
||||
HttpTimeout: ctx.Int("http-timeout"),
|
||||
Sources: ctx.String("sources"),
|
||||
Inclusions: ctx.String("inclusions"),
|
||||
Exclusions: ctx.String("exclusions"),
|
||||
SonarLogLevel: ctx.String("sonar-log-Level"),
|
||||
ShowProfiling: ctx.String("showProfiling"),
|
||||
BranchAnalysis: ctx.String("branchAnalysis"),
|
||||
UsingProperties: ctx.Bool("usingProperties"),
|
||||
Binaries: ctx.String("binaries"),
|
||||
Quality: ctx.String("quality-target"),
|
||||
QualityEnabled: ctx.Bool("quality-gate-enabled"),
|
||||
QualityTimeout: ctx.Int("quality-gate-timeout"),
|
||||
ArtifactFile: ctx.String("artifact-file"),
|
||||
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
bdata := plugLib.BuildDataFromContext(ctx, nil)
|
||||
|
||||
plug := plugin.Plugin{
|
||||
Config: &config,
|
||||
BuildData: &bdata,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
return plug
|
||||
}
|
||||
|
||||
func setupSugaredLogger(level string) *zap.SugaredLogger {
|
||||
|
||||
fmt.Printf("loglevel %s", level)
|
||||
|
||||
alvl, err := zap.ParseAtomicLevel(level)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("provided plugin log level failed to parse \"%s\": %w", level, err))
|
||||
}
|
||||
|
||||
conf := zap.NewProductionConfig()
|
||||
conf.EncoderConfig.TimeKey = "" // don't print a timestamp
|
||||
conf.EncoderConfig.CallerKey = "" // don't show caller
|
||||
conf.Level = alvl
|
||||
|
||||
if conf.Level.Level() == zap.DebugLevel {
|
||||
conf.EncoderConfig.CallerKey = "caller"
|
||||
conf.EncoderConfig.FunctionKey = "func"
|
||||
}
|
||||
|
||||
logger := zap.Must(conf.Build())
|
||||
defer logger.Sync()
|
||||
|
||||
sugar := logger.Sugar()
|
||||
|
||||
sugar.Infow("Logger setup", "levelConf", level)
|
||||
sugar.Debugw("debug level enabled")
|
||||
|
||||
return sugar
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
// Copyright 2022 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.kle.li/gapodo/woodpecker-plugin-lib/common"
|
||||
"git.kle.li/gapodo/woodpecker-plugin-sonar/pkg/sonar"
|
||||
"github.com/pelletier/go-toml"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
Plugin struct {
|
||||
Config *sonar.Config
|
||||
BuildData *common.BuildData
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
args struct {
|
||||
list []string
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Plugin) AnalyzeBranch() bool {
|
||||
return p.Config.BranchAnalysis == "true" || (p.Config.BranchAnalysis == "auto" && !p.SourceBranchIsDefault())
|
||||
}
|
||||
|
||||
func (p *Plugin) SourceBranchIsDefault() bool {
|
||||
return p.BuildData.SourceBranch() == p.BuildData.Metadata.Repo.Branch || len(p.BuildData.SourceBranch()) == 0
|
||||
}
|
||||
|
||||
func (p *Plugin) getAuth() *sonar.Authentication {
|
||||
return &sonar.Authentication{
|
||||
Token: p.Config.Token,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) Exec() {
|
||||
|
||||
p.prepareVersion()
|
||||
p.runScanner()
|
||||
|
||||
report := p.staticScanToReport()
|
||||
|
||||
p.Logger.Infow("job url", "url", report.CeTaskURL)
|
||||
|
||||
task := sonar.WaitForSonarJob(report, p.getAuth(), p.Config)
|
||||
|
||||
p.Logger.Infow("Job finished", "report", report)
|
||||
|
||||
status := sonar.GetStatus(task, report, p.getAuth(), p.Config)
|
||||
p.Logger.Infow("status received", "status", status, "dashboardURL", p.Config.GetProjectDashUrl())
|
||||
|
||||
p.Logger.Infow("Woodpecker SonarQube Plugin",
|
||||
"qualityEnabled", p.Config.QualityEnabled)
|
||||
|
||||
if p.Config.QualityEnabled {
|
||||
p.Logger.Info("QualityGate ENABLED")
|
||||
|
||||
if status != p.Config.Quality {
|
||||
p.Logger.Fatal("QualityGate status FAILED",
|
||||
"targetQuality", p.Config.Quality,
|
||||
"qualityResult", status)
|
||||
} else {
|
||||
p.Logger.Infow("QualityGate PASSED", "status", status)
|
||||
}
|
||||
}
|
||||
|
||||
if !p.Config.QualityEnabled {
|
||||
p.Logger.Info("QualityGate DISABLED only displaying information")
|
||||
if status != p.Config.Quality {
|
||||
p.Logger.Infow("QualityGate status FAILED", "status", status)
|
||||
} else {
|
||||
p.Logger.Infow("QualityGate status PASSED", "status", status)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) runScanner() {
|
||||
scannerArgs := p.generateScannerArgs()
|
||||
|
||||
p.Logger.Debugw("Prepping env SONAR_USER_HOME")
|
||||
os.Setenv("SONAR_USER_HOME", ".sonar")
|
||||
|
||||
p.Logger.Infow("executing sonar-scanner", "options", strings.Join(scannerArgs, " "))
|
||||
|
||||
cmd := exec.Command("sonar-scanner", scannerArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
p.Logger.Fatalw("executing sonarscanner failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) generateScannerArgs() []string {
|
||||
args := &args{
|
||||
list: []string{
|
||||
"-Dsonar.host.url=" + p.Config.HostURL,
|
||||
"-Dsonar.login=" + p.Config.Token,
|
||||
},
|
||||
logger: p.Logger,
|
||||
}
|
||||
|
||||
if !p.Config.UsingProperties {
|
||||
argsParameter := []string{
|
||||
"-Dsonar.projectKey=" + p.Config.Key,
|
||||
"-Dsonar.projectName=" + p.Config.Name,
|
||||
"-Dsonar.projectVersion=" + p.Config.Version,
|
||||
"-Dsonar.sources=" + p.Config.Sources,
|
||||
"-Dsonar.ws.timeout=" + strconv.Itoa(p.Config.HttpTimeout),
|
||||
"-Dsonar.log.level=" + p.Config.SonarLogLevel,
|
||||
"-Dsonar.showProfiling=" + p.Config.ShowProfiling,
|
||||
"-Dsonar.scm.provider=" + p.BuildData.AdditionalData.SCM,
|
||||
}
|
||||
args.append(argsParameter...)
|
||||
|
||||
args.appendIfNotEmpty("-Dsonar.java.binaries=", p.Config.Binaries)
|
||||
args.appendIfNotEmpty("-Dsonar.exclusions=", p.Config.Exclusions)
|
||||
args.appendIfNotEmpty("-Dsonar.inclusions=", p.Config.Inclusions)
|
||||
|
||||
}
|
||||
|
||||
p.Logger.Debugw("argsbuilder",
|
||||
"analyze", p.AnalyzeBranch(),
|
||||
"isPr", p.BuildData.IsPR(),
|
||||
"isTag", p.BuildData.IsTag(),
|
||||
"source", p.BuildData.SourceBranch(),
|
||||
"target", p.BuildData.TargetBranch(),
|
||||
"prKey", p.BuildData.PullRequest())
|
||||
|
||||
if p.AnalyzeBranch() && !p.BuildData.IsPR() {
|
||||
args.appendMustIfNotEmpty("-Dsonar.branch.name=", p.BuildData.TargetBranch())
|
||||
}
|
||||
|
||||
if p.AnalyzeBranch() && p.BuildData.IsPR() {
|
||||
args.appendMustIfNotEmpty("-Dsonar.pullrequest.branch=", p.BuildData.SourceBranch())
|
||||
args.appendMustIfNotEmpty("-Dsonar.pullrequest.base=", p.BuildData.TargetBranch())
|
||||
args.appendMustIfNotEmpty("-Dsonar.pullrequest.key=", p.BuildData.PullRequest())
|
||||
}
|
||||
|
||||
args.appendIfNotEmpty("-Dsonar.qualitygate.wait=", strconv.FormatBool(p.Config.QualityEnabled))
|
||||
args.appendIfNotEmpty("-Dsonar.qualitygate.timeout=", strconv.Itoa(p.Config.QualityTimeout))
|
||||
|
||||
validArg := regexp.MustCompile(".*=.+")
|
||||
for _, argument := range args.list {
|
||||
if !validArg.Match([]byte(argument)) {
|
||||
p.Logger.Fatalf("verifying generateScannerArgs failed: %s does not have a value assigned", argument)
|
||||
}
|
||||
}
|
||||
|
||||
return args.list
|
||||
}
|
||||
|
||||
func (p *Plugin) staticScanToReport() *sonar.Report {
|
||||
|
||||
var reportLocation = ".scannerwork/report-task.txt"
|
||||
|
||||
fileData, err := os.ReadFile(reportLocation)
|
||||
if err != nil {
|
||||
p.Logger.Fatalw("failed to access report-task file",
|
||||
"filepath", reportLocation,
|
||||
"error", err)
|
||||
}
|
||||
|
||||
replacer := regexp.MustCompile("(?m)^(.*?)=(.*)($)")
|
||||
data := replacer.ReplaceAllString(string(fileData), "$1=\"$2\"")
|
||||
|
||||
report := sonar.Report{}
|
||||
|
||||
if err = toml.Unmarshal([]byte(data), &report); err != nil {
|
||||
p.Logger.Fatalw("failed to unmarshal task-report toml to Report", "error", err)
|
||||
}
|
||||
|
||||
return &report
|
||||
}
|
||||
|
||||
func (p *Plugin) prepareVersion() {
|
||||
if len(p.Config.Version) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if p.BuildData.IsTag() {
|
||||
p.Config.Version = p.BuildData.Tag()
|
||||
p.Logger.Infow("Version unset updating it",
|
||||
"action", "tag",
|
||||
"version", p.Config.Version)
|
||||
return
|
||||
}
|
||||
|
||||
if p.BuildData.IsPR() {
|
||||
p.Config.Version = p.BuildData.SourceBranch()
|
||||
p.Logger.Infow("Version unset updating it",
|
||||
"action", "pull request",
|
||||
"version", p.Config.Version)
|
||||
return
|
||||
}
|
||||
|
||||
p.Config.Version = p.BuildData.TargetBranch()
|
||||
}
|
||||
|
||||
func (a *args) appendMustIfNotEmpty(prefix string, value string) {
|
||||
if len(value) > 0 {
|
||||
a.append(prefix + value)
|
||||
} else {
|
||||
a.logger.Fatalw("error appending argument, value empty",
|
||||
"prefix", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *args) appendIfNotEmpty(prefix string, value string) {
|
||||
if len(value) > 0 {
|
||||
a.append(prefix + value)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *args) append(elems ...string) {
|
||||
a.list = append(a.list, elems...)
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
// Copyright 2022 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sonar
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
DashboardURL = "/dashboard?id="
|
||||
)
|
||||
|
||||
var netClient *http.Client
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Key string
|
||||
Name string
|
||||
HostURL string
|
||||
Token string
|
||||
|
||||
Version string
|
||||
Sources string
|
||||
HttpTimeout int
|
||||
Inclusions string
|
||||
Exclusions string
|
||||
SonarLogLevel string
|
||||
ShowProfiling string
|
||||
BranchAnalysis string
|
||||
UsingProperties bool
|
||||
|
||||
Binaries string
|
||||
Quality string
|
||||
QualityEnabled bool
|
||||
QualityTimeout int
|
||||
ArtifactFile string
|
||||
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
Authentication struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
Report struct {
|
||||
ProjectKey string `toml:"projectKey"`
|
||||
ServerURL string `toml:"serverUrl"`
|
||||
DashboardURL string `toml:"dashboardUrl"`
|
||||
CeTaskID string `toml:"ceTaskId"`
|
||||
CeTaskURL string `toml:"ceTaskUrl"`
|
||||
}
|
||||
|
||||
TaskResponse struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
ComponentID string `json:"componentId"`
|
||||
ComponentKey string `json:"componentKey"`
|
||||
ComponentName string `json:"componentName"`
|
||||
AnalysisID string `json:"analysisId"`
|
||||
Status string `json:"status"`
|
||||
} `json:"task"`
|
||||
}
|
||||
|
||||
ProjectStatusResponse struct {
|
||||
ProjectStatus struct {
|
||||
Status string `json:"status"`
|
||||
} `json:"projectStatus"`
|
||||
}
|
||||
|
||||
Project struct {
|
||||
ProjectStatus Status `json:"projectStatus"`
|
||||
}
|
||||
|
||||
Status struct {
|
||||
Status string `json:"status"`
|
||||
IgnoredConditions bool `json:"ignoredConditions"`
|
||||
Conditions []Condition `json:"conditions"`
|
||||
}
|
||||
|
||||
Condition struct {
|
||||
Status string `json:"status"`
|
||||
MetricKey string `json:"metricKey"`
|
||||
Comparator string `json:"comparator"`
|
||||
PeriodIndex int `json:"periodIndex"`
|
||||
ErrorThreshold string `json:"errorThreshold"`
|
||||
ActualValue string `json:"actualValue"`
|
||||
}
|
||||
|
||||
Testsuites struct {
|
||||
XMLName xml.Name `xml:"testsuites"`
|
||||
Text string `xml:",chardata"`
|
||||
TestSuite []Testsuite `xml:"testsuite"`
|
||||
}
|
||||
|
||||
Testsuite struct {
|
||||
Text string `xml:",chardata"`
|
||||
Package string `xml:"package,attr"`
|
||||
Time int `xml:"time,attr"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Errors int `xml:"errors,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
TestCase []Testcase `xml:"testcase"`
|
||||
}
|
||||
|
||||
Testcase struct {
|
||||
Text string `xml:",chardata"`
|
||||
Time int `xml:"time,attr"` // Actual Value Sonar
|
||||
Name string `xml:"name,attr"` // Metric Key
|
||||
Classname string `xml:"classname,attr"` // The metric Rule
|
||||
Failure *Failure `xml:"failure"` // Sonar Failure - show results
|
||||
}
|
||||
|
||||
Failure struct {
|
||||
Text string `xml:",chardata"`
|
||||
Message string `xml:"message,attr"`
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
netClient = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) GetProjectDashUrl() string {
|
||||
return c.HostURL + DashboardURL + c.Name
|
||||
}
|
||||
|
||||
func (a *Authentication) UrlToken() string {
|
||||
return base64.StdEncoding.EncodeToString([]byte((a.Token + ":")))
|
||||
}
|
||||
|
||||
func (a *Authentication) AddAuthHeader(r *http.Request) {
|
||||
r.Header.Add("Authorization", "Basic "+a.UrlToken())
|
||||
}
|
||||
|
||||
func GetSonarJobStatus(report *Report, auth *Authentication, config *Config) *TaskResponse {
|
||||
task := &TaskResponse{}
|
||||
sonarJsonRequest("GET", report.CeTaskURL, nil, auth, config, task)
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
func WaitForSonarJob(report *Report, auth *Authentication, config *Config) *TaskResponse {
|
||||
timeout := time.NewTimer(300 * time.Second)
|
||||
tick := time.NewTicker(500 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
tick.Stop()
|
||||
timeout.Stop()
|
||||
config.Logger.Fatalw("WaitForSonarJob timed out")
|
||||
case <-tick.C:
|
||||
job := GetSonarJobStatus(report, auth, config)
|
||||
|
||||
if job.Task.Status == "SUCCESS" {
|
||||
timeout.Stop()
|
||||
tick.Stop()
|
||||
return job
|
||||
}
|
||||
if job.Task.Status == "ERROR" {
|
||||
timeout.Stop()
|
||||
tick.Stop()
|
||||
config.Logger.Fatalw("WaitForSonarJob task failed with status: ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetStatus(task *TaskResponse, report *Report, auth *Authentication, config *Config) (status string) {
|
||||
reportRequest := url.Values{
|
||||
"analysisId": {task.Task.AnalysisID},
|
||||
}
|
||||
project := &ProjectStatusResponse{}
|
||||
|
||||
url := report.ServerURL + "/api/qualitygates/project_status?" + reportRequest.Encode()
|
||||
sonarJsonRequest("GET", url, nil, auth, config, project)
|
||||
|
||||
return project.ProjectStatus.Status
|
||||
}
|
||||
|
||||
func sonarJsonRequest(reqType string, url string, body io.Reader, auth *Authentication, config *Config, targetPointer any) {
|
||||
|
||||
rv := reflect.ValueOf(targetPointer)
|
||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||
config.Logger.Fatalw("invalid parameter, not a pointer", "type", reflect.TypeOf(targetPointer))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(reqType, url, body)
|
||||
if err != nil {
|
||||
config.Logger.Fatalw("failed to build request for url",
|
||||
"httpRequestType", reqType,
|
||||
"url", url,
|
||||
"targetType", reflect.TypeOf(targetPointer).String(),
|
||||
"error", err)
|
||||
}
|
||||
|
||||
auth.AddAuthHeader(req)
|
||||
resp, err := netClient.Do(req)
|
||||
if err != nil {
|
||||
config.Logger.Fatalw("failed getting data from server",
|
||||
"targetType", reflect.TypeOf(targetPointer).String(),
|
||||
"error", err)
|
||||
}
|
||||
|
||||
buf, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(buf, targetPointer); err != nil {
|
||||
config.Logger.Fatalw("failed to unmarshal response",
|
||||
"targetType", reflect.TypeOf(targetPointer).String(),
|
||||
"error", err)
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue