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