From dc3969cd05573e7eeab6178b8d66c894d81be5a7 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 22 May 2022 14:03:23 +0200 Subject: [PATCH] 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 --- .gitignore | 2 +- Dockerfile | 1 + README.md | 13 +++++++++ cmd/gitea-sonarqube-bot/main.go | 26 +++++++++-------- cmd/gitea-sonarqube-bot/main_test.go | 22 --------------- helm/Chart.yaml | 4 +-- helm/README.md | 41 ++++++++++++++++++--------- helm/templates/deployment.yaml | 14 ++++++++++ helm/values.yaml | 4 +++ internal/settings/settings.go | 10 +++---- internal/settings/settings_test.go | 42 +++++++++++++++------------- 11 files changed, 103 insertions(+), 76 deletions(-) delete mode 100644 cmd/gitea-sonarqube-bot/main_test.go diff --git a/.gitignore b/.gitignore index a3538d3..2a6750a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /vendor/ /node_modules/ /helm-releases/ -/gitea-sonarqube-bot +/gitea-sonarqube-bot* /coverage.html /*.log /cover.out diff --git a/Dockerfile b/Dockerfile index c91dd76..93bf360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 7507ed6..efb8c47 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 5c9eb4d..42b5de3 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -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) diff --git a/cmd/gitea-sonarqube-bot/main_test.go b/cmd/gitea-sonarqube-bot/main_test.go deleted file mode 100644 index b48aff4..0000000 --- a/cmd/gitea-sonarqube-bot/main_test.go +++ /dev/null @@ -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") - }) -} diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 23adaee..13162e1 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -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 diff --git a/helm/README.md b/helm/README.md index 4e929a1..3cd5ccc 100644 --- a/helm/README.md +++ b/helm/README.md @@ -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. diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 2a04847..481b9d3 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -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 }} diff --git a/helm/values.yaml b/helm/values.yaml index 6ac32a8..377758b 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -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 diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 1e1a3d0..373ccaf 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -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 { diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 91adf57..168c04f 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -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") }