Improve configuration file flexibility

Instead of re-inventing the wheel regarding configuration location
handling and validation, this introduces a new command flag `--config`
allowing for full flexibility of configuration filename and location.
This flag can also be defined via environment variable which allows
an easy way of starting the bot from command line, inside a Docker
container or using the Helm Chart.

It makes the custom environment lookup unnecessary and reduces some
complexity during startup and for writing tests.

Resolves: #10

Signed-off-by: Steven Kriegler <sk.bunsenbrenner@gmail.com>
This commit is contained in:
justusbunsi 2022-05-22 14:03:23 +02:00
parent 5cb3daab60
commit dc3969cd05
No known key found for this signature in database
GPG key ID: 82B29BF2507F9F8B
11 changed files with 103 additions and 76 deletions

2
.gitignore vendored
View file

@ -6,7 +6,7 @@
/vendor/
/node_modules/
/helm-releases/
/gitea-sonarqube-bot
/gitea-sonarqube-bot*
/coverage.html
/*.log
/cover.out

View file

@ -40,6 +40,7 @@ ENV HOME=/home/bot
EXPOSE 3000
ENV GIN_MODE "release"
ENV GITEA_SQ_BOT_CONFIG_PATH "/home/bot/config/config.yaml"
VOLUME ["/home/bot/config/"]
RUN ["chmod", "+x", "/usr/local/bin/docker-entrypoint.sh"]

View file

@ -61,6 +61,19 @@ and execute the following (replace `$TAG` first):
docker run --rm -it -p 9000:3000 -v "$(pwd)/config/:/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG
```
**Starting with v0.2.0**
By default, the bot expects its configuration file under `./config/config.yaml` next to the bot executable. Inside the Docker image the
corresponding full path is `/home/bot/config/config.yaml`. If you prefer using a different location or even a different filename, you can
also define the environment variable `GITEA_SQ_BOT_CONFIG_PATH` that allows for changing that full path.
Imagine having a `./config/sqbot.config.yml` on your host that you want to populate inside `/mnt/`, the correct command to run a Docker
container would be:
```bash
docker run --rm -it -p 9000:3000 -e "GITEA_SQ_BOT_CONFIG_PATH=/mnt/sqbot.config.yml" -v "$(pwd)/config/:/mnt/" justusbunsi/gitea-sonarqube-bot:$TAG
```
### Helm Chart
See [Helm Chart README](helm/README.md) for detailed instructions.

View file

@ -4,7 +4,6 @@ import (
"fmt"
"log"
"os"
"path"
"gitea-sonarqube-pr-bot/internal/api"
giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea"
@ -15,23 +14,22 @@ import (
"github.com/urfave/cli/v2"
)
func getConfigLocation() string {
configPath := path.Join("config")
if customConfigPath, ok := os.LookupEnv("PRBOT_CONFIG_PATH"); ok {
configPath = customConfigPath
}
return configPath
}
func main() {
settings.Load(getConfigLocation())
app := &cli.App{
Name: "gitea-sonarqube-pr-bot",
Usage: "Improve your experience with SonarQube and Gitea",
Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`,
Action: serveApi,
Flags: []cli.Flag{
&cli.PathFlag{
Name: "config",
Aliases: []string{"c"},
Value: "./config/config.yaml",
Usage: "Full path to configuration file.",
EnvVars: []string{"GITEA_SQ_BOT_CONFIG_PATH"},
TakesFile: true,
},
},
}
err := app.Run(os.Args)
@ -43,6 +41,10 @@ func main() {
func serveApi(c *cli.Context) error {
fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.")
config := c.Path("config")
settings.Load(config)
fmt.Printf("Config file in use: %s\n", config)
giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New())
sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New())
server := api.New(giteaHandler, sqHandler)

View file

@ -1,22 +0,0 @@
package main
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetConfigLocationWithDefault(t *testing.T) {
assert.Equal(t, "config", getConfigLocation())
}
func TestGetConfigLocationWithEnvironmentOverride(t *testing.T) {
os.Setenv("PRBOT_CONFIG_PATH", "/tmp/")
assert.Equal(t, "/tmp/", getConfigLocation())
t.Cleanup(func() {
os.Unsetenv("PRBOT_CONFIG_PATH")
})
}

View file

@ -2,8 +2,8 @@ apiVersion: v2
name: gitea-sonarqube-bot
description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube
type: application
version: 0.1.2
appVersion: "v0.1.1"
version: 0.2.0
appVersion: "v0.2.0"
home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/
maintainers:
- name: Steven Kriegler

View file

@ -1,5 +1,19 @@
# Gitea SonarQube Bot
_Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers
being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately,
this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327).
_Gitea SonarQube Bot_ aims to fill the gap between working on pull requests and being notified on quality changes.
- [Gitea SonarQube Bot](#gitea-sonarqube-bot)
- [Installation](#installation)
- [Parameters](#parameters)
- [Common parameters](#common-parameters)
- [App parameters](#app-parameters)
- [Security parameters](#security-parameters)
- [Traffic exposure parameters](#traffic-exposure-parameters)
- [License](#license)
## Installation
```bash
@ -35,18 +49,19 @@ for full configuration options.
### App parameters
| Name | Description | Value |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` |
| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` |
| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` |
| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` |
| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` |
| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` |
| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` |
| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` |
| Name | Description | Value |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` |
| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` |
| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` |
| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` |
| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` |
| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` |
| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` |
| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` |
| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` |
### Security parameters
@ -79,4 +94,4 @@ for full configuration options.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text.
This project is licensed under the MIT License. See the [LICENSE](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/helm/LICENSE) file for the full license text.

View file

@ -31,6 +31,11 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.app.configLocationOverride }}
env:
- name: GITEA_SQ_BOT_CONFIG_PATH
value: "{{ .Values.app.configLocationOverride }}"
{{- end}}
ports:
- name: http
containerPort: 3000
@ -47,7 +52,11 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: sq-bot-config
{{- if .Values.app.configLocationOverride }}
mountPath: {{ dir .Values.app.configLocationOverride }}
{{- else }}
mountPath: /home/bot/config
{{- end }}
readOnly: true
{{- if .Values.volumeMounts }}
{{- toYaml .Values.volumeMounts | nindent 12 }}
@ -68,6 +77,11 @@ spec:
- name: sq-bot-config
secret:
secretName: {{ include "helm.fullname" . }}
{{- if .Values.app.configLocationOverride }}
items:
- key: config.yaml
path: {{ base .Values.app.configLocationOverride }}
{{- end }}
{{- if .Values.volumes }}
{{- toYaml .Values.volumes | nindent 8 }}
{{- end }}

View file

@ -53,6 +53,10 @@ podAnnotations: {}
# @section App parameters
app:
# @param app.configLocationOverride Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details)
# Setting this will also change the mount point for `.Values.app.configuration` to the directory part of the override value.
configLocationOverride: ""
# This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application.
configuration:
# Gitea related configuration. Necessary for adding/updating comments on repository pull requests

View file

@ -13,10 +13,9 @@ var (
Projects []Project
)
func newConfigReader() *viper.Viper {
func newConfigReader(configFile string) *viper.Viper {
v := viper.New()
v.SetConfigName("config.yaml")
v.SetConfigType("yaml")
v.SetConfigFile(configFile)
v.SetEnvPrefix("prbot")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AllowEmptyEnv(true)
@ -38,9 +37,8 @@ func newConfigReader() *viper.Viper {
return v
}
func Load(configPath string) {
r := newConfigReader()
r.AddConfigPath(configPath)
func Load(configFile string) {
r := newConfigReader(configFile)
err := r.ReadInConfig()
if err != nil {

View file

@ -31,7 +31,7 @@ projects:
name: pr-bot
`)
func WriteConfigFile(t *testing.T, content []byte) {
func WriteConfigFile(t *testing.T, content []byte) string {
dir := os.TempDir()
config := path.Join(dir, "config.yaml")
@ -40,21 +40,23 @@ func WriteConfigFile(t *testing.T, content []byte) {
})
_ = ioutil.WriteFile(config, content, 0444)
return config
}
func TestLoadWithMissingFile(t *testing.T) {
assert.Panics(t, func() { Load(os.TempDir()) }, "No panic while reading missing file")
assert.Panics(t, func() { Load(path.Join(os.TempDir(), "config.yaml")) }, "No panic while reading missing file")
}
func TestLoadWithExistingFile(t *testing.T) {
WriteConfigFile(t, defaultConfig)
c := WriteConfigFile(t, defaultConfig)
assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file")
assert.NotPanics(t, func() { Load(c) }, "Unexpected panic while reading existing file")
}
func TestLoadGiteaStructure(t *testing.T) {
WriteConfigFile(t, defaultConfig)
Load(os.TempDir())
c := WriteConfigFile(t, defaultConfig)
Load(c)
expected := GiteaConfig{
Url: "https://example.com/gitea",
@ -72,8 +74,8 @@ func TestLoadGiteaStructure(t *testing.T) {
func TestLoadGiteaStructureInjectedEnvs(t *testing.T) {
os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret")
os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token")
WriteConfigFile(t, defaultConfig)
Load(os.TempDir())
c := WriteConfigFile(t, defaultConfig)
Load(c)
expected := GiteaConfig{
Url: "https://example.com/gitea",
@ -94,8 +96,8 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) {
}
func TestLoadSonarQubeStructure(t *testing.T) {
WriteConfigFile(t, defaultConfig)
Load(os.TempDir())
c := WriteConfigFile(t, defaultConfig)
Load(c)
expected := SonarQubeConfig{
Url: "https://example.com/sonarqube",
@ -112,7 +114,7 @@ func TestLoadSonarQubeStructure(t *testing.T) {
}
func TestLoadSonarQubeStructureWithAdditionalMetrics(t *testing.T) {
WriteConfigFile(t, []byte(
c := WriteConfigFile(t, []byte(
`gitea:
url: https://example.com/gitea
token:
@ -129,7 +131,7 @@ projects:
owner: example-organization
name: pr-bot
`))
Load(os.TempDir())
Load(c)
expected := SonarQubeConfig{
Url: "https://example.com/sonarqube",
@ -152,8 +154,8 @@ projects:
func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) {
os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret")
os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token")
WriteConfigFile(t, defaultConfig)
Load(os.TempDir())
c := WriteConfigFile(t, defaultConfig)
Load(c)
expected := SonarQubeConfig{
Url: "https://example.com/sonarqube",
@ -186,7 +188,7 @@ func TestLoadStructureWithFileReferenceResolving(t *testing.T) {
sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube")
_ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`), 0444)
WriteConfigFile(t, []byte(
c := WriteConfigFile(t, []byte(
`gitea:
url: https://example.com/gitea
token:
@ -232,7 +234,7 @@ projects:
AdditionalMetrics: []string{},
}
Load(os.TempDir())
Load(c)
assert.EqualValues(t, expectedGitea, Gitea)
assert.EqualValues(t, expectedSonarQube, SonarQube)
@ -249,8 +251,8 @@ projects:
}
func TestLoadProjectsStructure(t *testing.T) {
WriteConfigFile(t, defaultConfig)
Load(os.TempDir())
c := WriteConfigFile(t, defaultConfig)
Load(c)
expectedProjects := []Project{
{
@ -283,7 +285,7 @@ sonarqube:
secret: haxxor-sonarqube-secret
projects: []
`)
WriteConfigFile(t, invalidConfig)
c := WriteConfigFile(t, invalidConfig)
assert.Panics(t, func() { Load(os.TempDir()) }, "No panic for empty project mapping that is required")
assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required")
}