From 24e3de8fc115565f487190d4fd5472d59ce7a8fa Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:50:30 +0200 Subject: [PATCH] Better config (#50) --- README.md | 72 +++++++++---- internal/config/config.go | 150 +++++++++++++++++++--------- internal/web/admin.go | 13 +-- internal/web/run.go | 4 +- public/admin.ts | 2 +- public/style.css | 12 +++ templates/base/admin_header.html | 4 +- templates/pages/admin_config.html | 116 +++++++++++++++++++++ templates/pages/admin_settings.html | 58 ----------- 9 files changed, 294 insertions(+), 137 deletions(-) create mode 100644 templates/pages/admin_config.html delete mode 100644 templates/pages/admin_settings.html diff --git a/README.md b/README.md index db1b02b..b4eced4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic * [With Docker](#with-docker) * [From source](#from-source) * [Configuration](#configuration) + * [Via YAML file](#configuration-via-yaml-file) + * [Via Environment Variables](#configuration-via-environment-variables) * [Administration](#administration) * [Use Nginx as a reverse proxy](#use-nginx-as-a-reverse-proxy) * [Use Fail2ban](#use-fail2ban) @@ -53,7 +55,7 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic A Docker [image](https://github.com/users/thomiceli/packages/container/package/opengist), available for each release, can be pulled -``` +```shell docker pull ghcr.io/thomiceli/opengist:1 ``` @@ -76,9 +78,6 @@ services: - "2222:2222" # SSH port, can be removed if you don't use SSH volumes: - "$HOME/.opengist:/root/.opengist" - environment: - CONFIG: | - log-level: info ``` ### From source @@ -96,29 +95,64 @@ Opengist is now running on port 6157, you can browse http://localhost:6157 ## Configuration -Opengist can be configured using YAML. The full configuration file is [config.yml](config.yml), each default key/value -pair can be overridden. +Opengist provides flexible configuration options through either a YAML file and/or environment variables. +You would only need to specify the configuration options you want to change — for any config option left untouched, Opengist will simply apply the default values. -### With docker +
+Configuration option list -Add a `CONFIG` environment variable in the `docker-compose.yml` file to the `opengist` service : +| YAML Config Key | Environment Variable | Default value | Description | +|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | +| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. | +| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | +| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | +| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | +| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | +| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | +| http.tls-enabled | OG_HTTP_TLS_ENABLED | `false` | Enable or disable TLS for the HTTP server. (`true` or `false`) | +| http.cert-file | OG_HTTP_CERT_FILE | none | Path to the TLS certificate file if TLS is enabled. | +| http.key-file | OG_HTTP_KEY_FILE | none | Path to the TLS key file if TLS is enabled. | +| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | +| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | +| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | +| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | +| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | +| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | +| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | +| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | +| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | +| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | -```diff -environment: - CONFIG: | - log-level: info - ssh.git-enabled: false - # ... -``` +
-### With binary +### Configuration via YAML file -Create a `config.yml` file (you can reuse this [one](config.yml)) and run Opengist binary with the `--config` flag : +The configuration file must be specified when launching the application, using the `--config` flag followed by the path to your YAML file. ```shell ./opengist --config /path/to/config.yml ``` +You can start by copying and/or modifying the provided [config.yml](config.yml) file. + +### Configuration via Environment Variables + +Usage with Docker Compose : + +```yml +services: + opengist: + # ... + environment: + OG_LOG_LEVEL: "info" + # etc. +``` +Usage via command line : + +```shell +OG_LOG_LEVEL=info ./opengist +``` ## Administration @@ -142,7 +176,7 @@ server { Then run : ```shell -service nginx restart +service nginx restart ``` ### Use Fail2ban @@ -172,7 +206,7 @@ port = anyport Then run ```shell -service fail2ban restart +service fail2ban restart ``` ## Configure OAuth diff --git a/internal/config/config.go b/internal/config/config.go index 6d3d205..bfd78d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" "os" "path/filepath" + "reflect" "strconv" "strings" ) @@ -18,30 +19,30 @@ var C *config // Not using nested structs because the library // doesn't support dot notation in this case sadly type config struct { - LogLevel string `yaml:"log-level"` - ExternalUrl string `yaml:"external-url"` - OpengistHome string `yaml:"opengist-home"` - DBFilename string `yaml:"db-filename"` + LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"` + ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"` + OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"` + DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` - HttpHost string `yaml:"http.host"` - HttpPort string `yaml:"http.port"` - HttpGit bool `yaml:"http.git-enabled"` - HttpTLSEnabled bool `yaml:"http.tls-enabled"` - HttpCertFile string `yaml:"http.cert-file"` - HttpKeyFile string `yaml:"http.key-file"` + HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"` + HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"` + HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"` + HttpTLSEnabled bool `yaml:"http.tls-enabled" env:"OG_HTTP_TLS_ENABLED"` + HttpCertFile string `yaml:"http.cert-file" env:"OG_HTTP_CERT_FILE"` + HttpKeyFile string `yaml:"http.key-file" env:"OG_HTTP_KEY_FILE"` - SshGit bool `yaml:"ssh.git-enabled"` - SshHost string `yaml:"ssh.host"` - SshPort string `yaml:"ssh.port"` - SshExternalDomain string `yaml:"ssh.external-domain"` - SshKeygen string `yaml:"ssh.keygen-executable"` + SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"` + SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"` + SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"` + SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"` + SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"` - GithubClientKey string `yaml:"github.client-key"` - GithubSecret string `yaml:"github.secret"` + GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"` + GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"` - GiteaClientKey string `yaml:"gitea.client-key"` - GiteaSecret string `yaml:"gitea.secret"` - GiteaUrl string `yaml:"gitea.url"` + GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"` + GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"` + GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"` } func configWithDefaults() (*config, error) { @@ -77,37 +78,12 @@ func InitConfig(configPath string) error { return err } - if configPath != "" { - absolutePath, _ := filepath.Abs(configPath) - absolutePath = filepath.Clean(absolutePath) - file, err := os.Open(absolutePath) - if err != nil { - if !os.IsNotExist(err) { - return err - } - fmt.Println("No YML config file found at " + absolutePath) - } else { - fmt.Println("Using config file: " + absolutePath) - - // Override default values with values from config.yml - d := yaml.NewDecoder(file) - if err = d.Decode(&c); err != nil { - return err - } - defer file.Close() - } - } else { - fmt.Println("No config file specified. Using default values.") + if err = loadConfigFromYaml(c, configPath); err != nil { + return err } - // Override default values with environment variables (as yaml) - configEnv := os.Getenv("CONFIG") - if configEnv != "" { - fmt.Println("Using config from environment variable: CONFIG") - d := yaml.NewDecoder(strings.NewReader(configEnv)) - if err = d.Decode(&c); err != nil { - return err - } + if err = loadConfigFromEnv(c); err != nil { + return err } C = c @@ -159,3 +135,79 @@ func GetHomeDir() string { absolutePath, _ := filepath.Abs(C.OpengistHome) return filepath.Clean(absolutePath) } + +func loadConfigFromYaml(c *config, configPath string) error { + if configPath != "" { + absolutePath, _ := filepath.Abs(configPath) + absolutePath = filepath.Clean(absolutePath) + file, err := os.Open(absolutePath) + if err != nil { + if !os.IsNotExist(err) { + return err + } + fmt.Println("No YAML config file found at " + absolutePath) + } else { + fmt.Println("Using YAML config file: " + absolutePath) + + // Override default values with values from config.yml + d := yaml.NewDecoder(file) + if err = d.Decode(&c); err != nil { + return err + } + defer file.Close() + } + } else { + fmt.Println("No YAML config file specified.") + } + + // Override default values with environment variables (as yaml) + configEnv := os.Getenv("CONFIG") + if configEnv != "" { + fmt.Println("Using config from environment variable: CONFIG") + d := yaml.NewDecoder(strings.NewReader(configEnv)) + if err := d.Decode(&c); err != nil { + return err + } + } + + return nil +} + +func loadConfigFromEnv(c *config) error { + v := reflect.ValueOf(c).Elem() + var envVars []string + + for i := 0; i < v.NumField(); i++ { + tag := v.Type().Field(i).Tag.Get("env") + + if tag == "" { + continue + } + + envValue := os.Getenv(strings.ToUpper(tag)) + if envValue == "" { + continue + } + + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(envValue) + case reflect.Bool: + boolVal, err := strconv.ParseBool(envValue) + if err != nil { + return err + } + v.Field(i).SetBool(boolVal) + } + + envVars = append(envVars, tag) + } + + if len(envVars) > 0 { + fmt.Println("Using environment variables config: " + strings.Join(envVars, ", ")) + } else { + fmt.Println("No environment variables config specified.") + } + + return nil +} diff --git a/internal/web/admin.go b/internal/web/admin.go index 2992565..9111f74 100644 --- a/internal/web/admin.go +++ b/internal/web/admin.go @@ -185,15 +185,16 @@ func adminSyncReposFromDB(ctx echo.Context) error { return redirect(ctx, "/admin-panel") } -func adminSettings(ctx echo.Context) error { - setData(ctx, "title", "Admin Settings") - setData(ctx, "htmlTitle", "Admin Settings - Admin panel") - setData(ctx, "adminHeaderPage", "settings") +func adminConfig(ctx echo.Context) error { + setData(ctx, "title", "Configuration") + setData(ctx, "htmlTitle", "Configuration - Admin panel") + setData(ctx, "adminHeaderPage", "config") + setData(ctx, "c", config.C) - return html(ctx, "admin_settings.html") + return html(ctx, "admin_config.html") } -func adminSetSetting(ctx echo.Context) error { +func adminSetConfig(ctx echo.Context) error { key := ctx.FormValue("key") value := ctx.FormValue("value") diff --git a/internal/web/run.go b/internal/web/run.go index dc5d0cc..00802c8 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -193,8 +193,8 @@ func Start() { g2.POST("/gists/:gist/delete", adminGistDelete) g2.POST("/sync-fs", adminSyncReposFromFS) g2.POST("/sync-db", adminSyncReposFromDB) - g2.GET("/settings", adminSettings) - g2.PUT("/set-setting", adminSetSetting) + g2.GET("/configuration", adminConfig) + g2.PUT("/set-config", adminSetConfig) } g1.GET("/all", allGists, checkRequireLogin) diff --git a/public/admin.ts b/public/admin.ts index dfdf21a..cb7a69e 100644 --- a/public/admin.ts +++ b/public/admin.ts @@ -12,7 +12,7 @@ const setSetting = (key: string, value: string) => { data.append('key', key); data.append('value', value); data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value)); - return fetch('/admin-panel/set-setting', { + return fetch('/admin-panel/set-config', { method: 'PUT', credentials: 'same-origin', body: data, diff --git a/public/style.css b/public/style.css index 1c222b6..93c98af 100644 --- a/public/style.css +++ b/public/style.css @@ -140,3 +140,15 @@ table.csv-table thead tr th { table.csv-table tbody td { @apply border py-1.5 px-1 border-slate-200 dark:border-slate-800; } + +dl.dl-config { + @apply grid grid-cols-3 text-sm; +} + +dl.dl-config dt { + @apply col-span-1 text-gray-700 dark:text-slate-300 font-bold; +} + +dl.dl-config dd { + @apply ml-1 col-span-2 break-words; +} diff --git a/templates/base/admin_header.html b/templates/base/admin_header.html index 0082d67..cecf911 100644 --- a/templates/base/admin_header.html +++ b/templates/base/admin_header.html @@ -15,8 +15,8 @@ {{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Users Gists - Admin settings + Configuration diff --git a/templates/pages/admin_config.html b/templates/pages/admin_config.html new file mode 100644 index 0000000..0cdb077 --- /dev/null +++ b/templates/pages/admin_config.html @@ -0,0 +1,116 @@ +{{ template "header" .}} +{{ template "admin_header" .}} + +
+
+

This configuration can be overridden by a YAML config file and/or environment variables.

+
+
+ +
+ General +
+
+
Log level
{{ .c.LogLevel }}
+
External URL
{{ .c.ExternalUrl }}
+
Opengist home
{{ .c.OpengistHome }}
+
DB filename
{{ .c.DBFilename }}
+
+ +
+ HTTP +
+
+
HTTP host
{{ .c.HttpHost }}
+
HTTP port
{{ .c.HttpPort }}
+
HTTP Git enabled
{{ .c.HttpGit }}
+
HTTP TLS enabled
{{ .c.HttpTLSEnabled }}
+
HTTP Cert file
{{ .c.HttpCertFile }}
+
HTTP Key file
{{ .c.HttpKeyFile }}
+
+ +
+ SSH +
+
+
SSH Git enabled
{{ .c.SshGit }}
+
SSH host
{{ .c.SshHost }}
+
SSH port
{{ .c.SshPort }}
+
SSH external domain
{{ .c.SshExternalDomain }}
+
SSH Keygen
{{ .c.SshKeygen }}
+
+ +
+ OAuth +
+
+
Github Client key
{{ .c.GithubClientKey }}
+
Github Secret
{{ .c.GithubSecret }}
+
Gitea client Key
{{ .c.GiteaClientKey }}
+
Gitea Secret
{{ .c.GiteaSecret }}
+
Gitea URL
{{ .c.GiteaUrl }}
+
+
+
+ + {{ .csrfHtml }} +
+
+ + + +{{ template "admin_footer" .}} +{{ template "footer" .}} diff --git a/templates/pages/admin_settings.html b/templates/pages/admin_settings.html deleted file mode 100644 index ba2831d..0000000 --- a/templates/pages/admin_settings.html +++ /dev/null @@ -1,58 +0,0 @@ -{{ template "header" .}} -{{ template "admin_header" .}} - -
- - - {{ .csrfHtml }} -
- - - -{{ template "admin_footer" .}} -{{ template "footer" .}}