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/ /vendor/
/node_modules/ /node_modules/
/helm-releases/ /helm-releases/
/gitea-sonarqube-bot /gitea-sonarqube-bot*
/coverage.html /coverage.html
/*.log /*.log
/cover.out /cover.out

View file

@ -40,6 +40,7 @@ ENV HOME=/home/bot
EXPOSE 3000 EXPOSE 3000
ENV GIN_MODE "release" ENV GIN_MODE "release"
ENV GITEA_SQ_BOT_CONFIG_PATH "/home/bot/config/config.yaml"
VOLUME ["/home/bot/config/"] VOLUME ["/home/bot/config/"]
RUN ["chmod", "+x", "/usr/local/bin/docker-entrypoint.sh"] 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 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 ### Helm Chart
See [Helm Chart README](helm/README.md) for detailed instructions. See [Helm Chart README](helm/README.md) for detailed instructions.

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path"
"gitea-sonarqube-pr-bot/internal/api" "gitea-sonarqube-pr-bot/internal/api"
giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea"
@ -15,23 +14,22 @@ import (
"github.com/urfave/cli/v2" "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() { func main() {
settings.Load(getConfigLocation())
app := &cli.App{ app := &cli.App{
Name: "gitea-sonarqube-pr-bot", Name: "gitea-sonarqube-pr-bot",
Usage: "Improve your experience with SonarQube and Gitea", 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.`, Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`,
Action: serveApi, 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) err := app.Run(os.Args)
@ -43,6 +41,10 @@ func main() {
func serveApi(c *cli.Context) error { func serveApi(c *cli.Context) error {
fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") 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()) giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New())
sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New())
server := api.New(giteaHandler, sqHandler) 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 name: gitea-sonarqube-bot
description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube
type: application type: application
version: 0.1.2 version: 0.2.0
appVersion: "v0.1.1" appVersion: "v0.2.0"
home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/
maintainers: maintainers:
- name: Steven Kriegler - name: Steven Kriegler

View file

@ -1,5 +1,19 @@
# Gitea SonarQube Bot # 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 ## Installation
```bash ```bash
@ -35,18 +49,19 @@ for full configuration options.
### App parameters ### App parameters
| Name | Description | Value | | 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.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.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | | `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.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | | `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.sonarqube.token.value` | SonarQube 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.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | | `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` |
| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | | `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` |
| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | | `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` |
| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | | `app.configuration.projects[0].gitea.owner` | Repository owner 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 | `[]` | | `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` |
| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | | `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 ### Security parameters
@ -79,4 +94,4 @@ for full configuration options.
## License ## 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 }} {{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.app.configLocationOverride }}
env:
- name: GITEA_SQ_BOT_CONFIG_PATH
value: "{{ .Values.app.configLocationOverride }}"
{{- end}}
ports: ports:
- name: http - name: http
containerPort: 3000 containerPort: 3000
@ -47,7 +52,11 @@ spec:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
volumeMounts: volumeMounts:
- name: sq-bot-config - name: sq-bot-config
{{- if .Values.app.configLocationOverride }}
mountPath: {{ dir .Values.app.configLocationOverride }}
{{- else }}
mountPath: /home/bot/config mountPath: /home/bot/config
{{- end }}
readOnly: true readOnly: true
{{- if .Values.volumeMounts }} {{- if .Values.volumeMounts }}
{{- toYaml .Values.volumeMounts | nindent 12 }} {{- toYaml .Values.volumeMounts | nindent 12 }}
@ -68,6 +77,11 @@ spec:
- name: sq-bot-config - name: sq-bot-config
secret: secret:
secretName: {{ include "helm.fullname" . }} secretName: {{ include "helm.fullname" . }}
{{- if .Values.app.configLocationOverride }}
items:
- key: config.yaml
path: {{ base .Values.app.configLocationOverride }}
{{- end }}
{{- if .Values.volumes }} {{- if .Values.volumes }}
{{- toYaml .Values.volumes | nindent 8 }} {{- toYaml .Values.volumes | nindent 8 }}
{{- end }} {{- end }}

View file

@ -53,6 +53,10 @@ podAnnotations: {}
# @section App parameters # @section App parameters
app: 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. # 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: configuration:
# Gitea related configuration. Necessary for adding/updating comments on repository pull requests # Gitea related configuration. Necessary for adding/updating comments on repository pull requests

View file

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

View file

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