🔧 Create a configuration file interface

This commit adds the ability to provide a config file in different ways:

The following options are given in the order in which they are checked:
1. A config file path passed by the -c parameter on the command line
2. A config file path passed by the HS_CONFIGFILE environment variable
3. A config.json file in the application root

Should these fail we try to get the parameters for the config struct
directly from environment variables or secret files as used in a
container environment. As example the JwtKey can be supplied by either
passing the value through:

* An environment variable HS_CONFIG_JWTKEY which contains the value directly
* An environment variable HS_CONFIG_JWTKEY_FILE which contains a path
  pointing to the Secret File
This commit is contained in:
Florian Beisel 2024-01-18 01:04:43 +01:00
parent c08de78984
commit 83753c3ee1
Signed by: florian
GPG Key ID: 79ECA2E54996FF4D
6 changed files with 107 additions and 5 deletions

5
.gitignore vendored
View File

@ -71,4 +71,7 @@ $RECYCLE.BIN/
!.gitsubmodules !.gitsubmodules
# sqlite database # sqlite database
*.db *.db
# config file
config.json

View File

@ -15,7 +15,7 @@ func GenerateToken(username string) (string, error) {
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(config.JwtKey) tokenString, err := token.SignedString(config.GlobalConfig.JwtKey)
return tokenString, err return tokenString, err
} }

4
config.example.json Normal file
View File

@ -0,0 +1,4 @@
{
"jwtKey": "your_secret_key",
"databaseFile": "hostname-service.db"
}

View File

@ -1,3 +1,89 @@
package config package config
var JwtKey = []byte("your_secret_key") import (
"encoding/json"
"errors"
"flag"
"io/ioutil"
"os"
"reflect"
"strings"
// You might need additional imports for file handling, etc.
)
type AppConfig struct {
JwtKey string
DatabaseFile string
}
var GlobalConfig *AppConfig
func LoadConfig() error {
GlobalConfig = &AppConfig{}
// Check command line arguments
configFile := flag.String("c", "", "path to config file")
flag.Parse()
if *configFile != "" {
return loadConfigFromFile(*configFile, GlobalConfig)
}
// Check environment variable for config file path
envConfigFile := os.Getenv("HS_CONFIGFILE")
if envConfigFile != "" {
return loadConfigFromFile(envConfigFile, GlobalConfig)
}
// Try to load from a default config file
defaultConfigFile := "config.json" // Replace with your default config file name
if _, err := os.Stat(defaultConfigFile); err == nil {
return loadConfigFromFile(defaultConfigFile, GlobalConfig)
}
// Load config from environment variables or docker secrets
loadConfigFromEnvOrSecrets(GlobalConfig)
// Check if the configuration has been loaded successfully
if GlobalConfig.JwtKey == "" || GlobalConfig.DatabaseFile == "" {
// Add more checks as necessary for other required config fields
return errors.New("failed to load configuration from any source")
}
return nil
}
func loadConfigFromFile(filePath string, config *AppConfig) error {
fileData, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
err = json.Unmarshal(fileData, config)
if err != nil {
return err
}
return nil
}
func loadConfigFromEnvOrSecrets(config *AppConfig) {
val := reflect.ValueOf(config).Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
envVar := "HS_CONFIG_" + strings.ToUpper(field.Name)
if value, exists := os.LookupEnv(envVar); exists {
val.Field(i).SetString(value)
}
// Handling Docker secrets (file-based secrets)
secretFileEnvVar := envVar + "_FILE"
if secretFilePath, exists := os.LookupEnv(secretFileEnvVar); exists {
if secretValue, err := ioutil.ReadFile(secretFilePath); err == nil {
val.Field(i).SetString(string(secretValue))
}
}
}
}

11
main.go
View File

@ -1,8 +1,11 @@
package main package main
import ( import (
"log"
"git.beisel.it/florian/hostname-service/api" "git.beisel.it/florian/hostname-service/api"
"git.beisel.it/florian/hostname-service/auth" "git.beisel.it/florian/hostname-service/auth"
"git.beisel.it/florian/hostname-service/config"
"git.beisel.it/florian/hostname-service/db" "git.beisel.it/florian/hostname-service/db"
"git.beisel.it/florian/hostname-service/docs" "git.beisel.it/florian/hostname-service/docs"
"git.beisel.it/florian/hostname-service/middleware" "git.beisel.it/florian/hostname-service/middleware"
@ -29,9 +32,15 @@ import (
// @description Type "Bearer" followed by a space and JWT token. // @description Type "Bearer" followed by a space and JWT token.
func main() { func main() {
err := config.LoadConfig()
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
db.Init("hostname-service.db") db.Init(config.GlobalConfig.DatabaseFile)
router := gin.Default() router := gin.Default()

View File

@ -23,7 +23,7 @@ func Authenticate() gin.HandlerFunc {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method") return nil, fmt.Errorf("unexpected signing method")
} }
return config.JwtKey, nil return config.GlobalConfig.JwtKey, nil
}) })
if err != nil { if err != nil {