config

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 6, 2026 License: Apache-2.0 Imports: 21 Imported by: 1

README

Test

Config Module

A flexible configuration management module for Go applications with support for YAML/JSON files, environment variables, and type-safe access to configuration values.

Installation

go get github.com/inovacc/config

Features

  • Load configuration from YAML or JSON files
  • Type-safe access to service-specific configuration using generics
  • Support for environment variable overrides with custom prefixes
  • Secure handling of sensitive configuration values
  • Automatic generation of default configuration files with sensible defaults
  • Built-in validation for configuration values
  • Structured logging integration
  • Based on a customized version of Viper for configuration management

Quick Start

Loading and Using Configuration
package main

import (
	"fmt"
	"log"

	"github.com/inovacc/config"
)

type ServiceConfig struct {
	Port int    `yaml:"port"`
	Host string `yaml:"host"`
}

func main() {
	// Initialize with default values
	svc := &ServiceConfig{
		Port: 8080,
		Host: "localhost",
	}

	// Load configuration from a file, applying defaults if needed
	if err := config.InitServiceConfig(svc, "config.yaml"); err != nil {
		log.Fatalf("Failed to load config: %v", err)
	}

	// Get the loaded configuration with type safety
	cfg, err := config.GetServiceConfig[*ServiceConfig]()
	if err != nil {
		log.Fatalf("Failed to get service config: %v", err)
	}

	fmt.Printf("Service running on %s:%d\n", cfg.Host, cfg.Port)

	// Access base configuration
	baseCfg := config.GetBaseConfig()
	fmt.Printf("Application ID: %s\n", baseCfg.AppID)
}

Advanced Features

Validation Rules and Default Values

The config module includes built-in validation for configuration values:

  • AppID: Must be at least 8 characters long. If not provided, a UUID is automatically generated.
  • AppSecret: Must be at least 12 characters long. If not provided, a UUID is automatically generated.
  • Logger.LogLevel: Must be one of "DEBUG", "INFO", "WARN", "WARNING", or "ERROR" (case-insensitive).
Creating Default Configuration

You can generate a default configuration file with random credentials using the DefaultConfig function:

// Generate a default config file with a zeroed MyServiceConfig
if err := config.DefaultConfig[*MyServiceConfig]("config.yaml"); err != nil {
    log.Fatal(err)
}
Environment Variable Overrides

You can override configuration values using environment variables:

// Set a prefix for environment variables
config.SetEnvPrefix("APP")

// Now environment variables like APP_PORT will override config values
// For example, setting APP_LOGGER_LOGLEVEL=INFO will override logger.logLevel
Secure Handling of Sensitive Values

Mark any string field with sensitive:"true" and it will be automatically masked in secure copies. This works for both the base AppSecret field and any fields in your service configuration struct:

type MyConfig struct {
    Username string `yaml:"username"`
    Password string `yaml:"password" sensitive:"true"`
}

// Get a copy with sensitive values masked (AppSecret + Password both masked)
secureCfg := config.GetSecureCopy()

// Log configuration safely
config.LogConfig()
Custom Validation Rules

Register custom validators that run during InitServiceConfig after built-in validation:

config.AddValidator(func(cfg config.Config) error {
    svc, ok := cfg.Service.(*MyServiceConfig)
    if !ok {
        return fmt.Errorf("unexpected service config type")
    }
    if svc.Port < 1024 || svc.Port > 65535 {
        return fmt.Errorf("port must be between 1024 and 65535, got %d", svc.Port)
    }
    return nil
})
Configuration Profiles

Profile-specific config files are automatically merged on top of the base config. The profile is determined by the environment field. For example, if environment: prod and the base config is config.yaml, the library looks for config.prod.yaml in the same directory:

# config.yaml (base)
environment: prod
logger:
  logLevel: DEBUG

# config.prod.yaml (profile override — optional)
logger:
  logLevel: ERROR

The profile file is optional — if it doesn't exist, the base config is used as-is.

Configuration Reloading

Watch the config file for changes and automatically reload:

config.WatchConfig(func() {
    log.Println("config reloaded")
})

On each file change, the library re-reads the config, merges any profile overrides, and runs custom validators. If validation fails, the change is rejected and the previous valid config is preserved.

Configuration Encryption

Encrypt sensitive values at rest using AES-256-GCM. Encrypted values are stored as ENC[base64data] in config files and transparently decrypted during loading:

// Set the encryption key (from env var, vault, etc.)
config.SetEncryptionKey([]byte(os.Getenv("CONFIG_KEY")))

// Encrypt a value for storage in a config file
encrypted, err := config.EncryptValue("my-database-password")
// encrypted = "ENC[base64...]"

// In your config.yaml:
// service:
//   password: ENC[base64...]

// Values are automatically decrypted during InitServiceConfig
Configuration Versioning & Migration

Support for versioning config files and migrating between schema versions:

// Set the expected config version
config.SetTargetVersion(3)

// Register migrations (run in order during InitServiceConfig)
config.AddMigration(1, 2, func(data map[string]any) error {
    // Transform config data from v1 to v2
    data["version"] = 2
    if logger, ok := data["logger"].(map[string]any); ok {
        logger["format"] = "json" // Add new field
    }
    return nil
})

config.AddMigration(2, 3, func(data map[string]any) error {
    data["version"] = 3
    return nil
})

Add a version field to your config file:

version: 1
appID: my-app-id-12345678
# ...

Project Structure

github.com/inovacc/config/
├── config.go          # Main implementation (init, get, validate, profiles, watch)
├── encrypt.go         # AES-256-GCM encryption/decryption for config values
├── migrate.go         # Configuration versioning and migration chain
├── config_test.go     # Core tests
├── encrypt_test.go    # Encryption tests
├── migrate_test.go    # Migration tests
├── benchmark_test.go  # Performance benchmarks
├── example_test.go    # Example tests (living documentation)
├── go.mod             # Module definition
├── internal/          # Internal packages
│   └── viper/         # Customized version of Viper
├── IMPROVEMENTS.md    # Completed improvements log
├── Taskfile.yml       # Task runner configuration
└── testdata/          # Test data (YAML and JSON samples)

Configuration Example

Below is an example of a config.yml file based on the module's structure:

appversion: 0.0.0-development
environment: dev
appID: 3222706d-aa89-4737-a6e3-46d29a7b8b02
appSecret: a6e780be-8b0b-4f5d-b907-72ae0d651eb8
logger:
  logLevel: DEBUG
service:
  username: ""
  password: ""

Improvements

All planned improvements have been implemented. See IMPROVEMENTS.md for the full list of completed features.

Acknowledgments

This project is based on a customized version of Viper, originally created by Steve Francia (@spf13). We would like to express our gratitude to Steve and all the contributors to the Viper project for their excellent work.

Documentation

Overview

Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/inovacc/config"
)

type ExampleServiceConfig struct {
	Port int    `yaml:"port"`
	Host string `yaml:"host"`
}

func main() {
	dir, _ := os.MkdirTemp("", "example-*")
	defer func() { _ = os.RemoveAll(dir) }()

	cfgPath := filepath.Join(dir, "config.yaml")
	_ = os.WriteFile(cfgPath, []byte(`
appID: example-app-id-12345
appSecret: example-secret-12345678
logger:
  logLevel: INFO
service:
  port: 8080
  host: localhost
`), 0644)

	svc := &ExampleServiceConfig{}

	if err := config.InitServiceConfig(svc, cfgPath); err != nil {
		fmt.Println("error:", err)
		return
	}

	cfg, err := config.GetServiceConfig[*ExampleServiceConfig]()
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	base := config.GetBaseConfig()
	fmt.Printf("AppID: %s\n", base.AppID)
	fmt.Printf("Host: %s, Port: %d\n", cfg.Host, cfg.Port)

}
Output:
AppID: example-app-id-12345
Host: localhost, Port: 8080

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddMigration

func AddMigration(from, to int, fn MigrationFunc)

AddMigration registers a migration function that transforms config data from version `from` to version `to`. Migrations are run in order during InitServiceConfig when the config file's version is older than the target version.

Example:

config.AddMigration(1, 2, func(data map[string]any) error {
    // Rename "logLevel" to "log_level" in logger section
    if logger, ok := data["logger"].(map[string]any); ok {
        if level, exists := logger["logLevel"]; exists {
            logger["log_level"] = level
            delete(logger, "logLevel")
        }
    }
    data["version"] = 2
    return nil
})

func AddValidator

func AddValidator(fn ValidatorFunc)

AddValidator registers a custom validation function that will be called during InitServiceConfig after the built-in validation completes.

Validators receive a read-only copy of the Config and should return an error if validation fails. Multiple validators can be registered and they run in order.

Must be called before InitServiceConfig.

Example:

config.AddValidator(func(cfg config.Config) error {
    svc, ok := cfg.Service.(*MyServiceConfig)
    if !ok {
        return fmt.Errorf("unexpected service config type")
    }
    if svc.Port < 1024 || svc.Port > 65535 {
        return fmt.Errorf("port must be between 1024 and 65535, got %d", svc.Port)
    }
    return nil
})
Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/inovacc/config"
)

type ExampleServiceConfig struct {
	Port int    `yaml:"port"`
	Host string `yaml:"host"`
}

func main() {
	dir, _ := os.MkdirTemp("", "example-validator-*")
	defer func() { _ = os.RemoveAll(dir) }()

	cfgPath := filepath.Join(dir, "config.yaml")
	_ = os.WriteFile(cfgPath, []byte(`
appID: example-app-id-12345
appSecret: example-secret-12345678
logger:
  logLevel: INFO
service:
  port: 80
  host: localhost
`), 0644)

	config.AddValidator(func(cfg config.Config) error {
		svc, ok := cfg.Service.(*ExampleServiceConfig)
		if !ok {
			return fmt.Errorf("unexpected service type")
		}
		if svc.Port < 1024 {
			return fmt.Errorf("port must be >= 1024, got %d", svc.Port)
		}
		return nil
	})

	err := config.InitServiceConfig(&ExampleServiceConfig{}, cfgPath)
	fmt.Println(err)

}
Output:
custom validation: port must be >= 1024, got 80

func DecryptValue

func DecryptValue(value string) (string, error)

DecryptValue decrypts a value in the format ENC[base64data] and returns the plaintext string. If the value is not encrypted (no ENC[...] wrapper), it is returned unchanged.

Example:

plain, err := config.DecryptValue("ENC[base64...]")

func DefaultConfig

func DefaultConfig[T any](configPath string) error

DefaultConfig generates a base configuration file with random credentials and zeroed service configuration for a given type.

It should be used to bootstrap a config.yaml with sensible defaults.

Example:

err := config.DefaultConfig[*MyServiceConfig]("config.yaml")
if err != nil {
    log.Fatal(err)
}

func EncryptValue

func EncryptValue(plaintext string) (string, error)

EncryptValue encrypts a plaintext string and returns it in the format ENC[base64data]. The encryption key must be set first via SetEncryptionKey.

Use this to prepare values before storing them in a config file.

Example:

encrypted, err := config.EncryptValue("my-secret-password")
// encrypted = "ENC[base64...]"

func GetConfigVersion

func GetConfigVersion() int

GetConfigVersion returns the current version from the loaded configuration.

func GetServiceConfig

func GetServiceConfig[T any]() (T, error)

GetServiceConfig returns the previously registered service-specific configuration with type safety using generics.

If the type does not match what was stored, an error is returned.

Example:

cfg, err := config.GetServiceConfig[*MyServiceConfig]()
if err != nil {
    log.Fatal(err)
}

func InitServiceConfig

func InitServiceConfig(v any, configPath string) error

InitServiceConfig loads a configuration file and binds a service-specific struct to the `Service` field in the global config.

It must be called before accessing the service configuration via GetServiceConfig.

If the configuration file does not exist, a default one will be created. Default values from the provided service config struct will be used if corresponding values are not found in the configuration file.

After loading, if a profile-specific config file exists (e.g., "config.prod.yaml" when Environment is "prod"), its values are merged on top of the base config.

Example:

type MyServiceConfig struct {
    Port int
    Mode string
}

svc := &MyServiceConfig{
    Port: 8080,  // Default value
    Mode: "dev", // Default value
}

err := config.InitServiceConfig(svc, "config.yaml")
if err != nil {
    log.Fatal(err)
}

func IsEncryptedValue

func IsEncryptedValue(s string) bool

IsEncryptedValue reports whether s is in the ENC[...] format.

func LogConfig

func LogConfig()

LogConfig logs the configuration at debug level, masking sensitive values.

This is a convenience method for safely logging the configuration.

Example:

config.LogConfig()

func SetEncryptionKey

func SetEncryptionKey(key []byte)

SetEncryptionKey sets the key used for encrypting and decrypting configuration values. The key can be any length; it is hashed with SHA-256 to produce a 32-byte AES-256 key.

Must be called before InitServiceConfig if the config file contains encrypted values.

Example:

config.SetEncryptionKey([]byte(os.Getenv("CONFIG_KEY")))

func SetEnvPrefix

func SetEnvPrefix(prefix string)

SetEnvPrefix sets a prefix for environment variables.

Environment variables that match the pattern {prefix}_* will override the corresponding configuration values. The matching is case-insensitive.

For example, if the prefix is "APP", then the environment variable "APP_LOGGER_LOGLEVEL" will override the value of "logger.logLevel" in the configuration file.

Must be called before InitServiceConfig.

Example:

config.SetEnvPrefix("APP")

func SetTargetVersion

func SetTargetVersion(version int)

SetTargetVersion sets the expected config version. During InitServiceConfig, if the config file's version is lower than the target, registered migrations are applied in order. If no target is set, migrations are skipped.

Example:

config.SetTargetVersion(3)

func WatchConfig

func WatchConfig(onChange ...func())

WatchConfig starts watching the configuration file for changes. When the file is modified, it is automatically re-read and the global configuration is updated. The optional onChange callback is invoked after each successful reload.

WatchConfig must be called after InitServiceConfig. It launches a background goroutine and returns immediately.

Example:

config.WatchConfig(func() {
    log.Println("config reloaded")
})

Types

type Config

type Config struct {
	Version     int    `yaml:"version" json:"version" mapstructure:"version"`
	Environment string `yaml:"environment" json:"environment" mapstructure:"environment"`
	AppVersion  string `yaml:"-" json:"-" mapstructure:"-"`
	ConfigFile  string `yaml:"-" json:"-" mapstructure:"-"`
	AppID       string `yaml:"appID" json:"appID" mapstructure:"appID"`
	AppSecret   string `yaml:"appSecret" json:"appSecret" mapstructure:"appSecret" sensitive:"true"`
	Logger      Logger `yaml:"logger" json:"logger" mapstructure:"logger"`
	Service     any    `yaml:"service" json:"service" mapstructure:"service"`
	// contains filtered or unexported fields
}

Config represents the global application configuration.

Fields:

  • Environment: The current environment (e.g., "dev", "prod").
  • AppVersion: The application version.
  • AppID: Unique application identifier.
  • AppSecret: Secret key for the application (sensitive).
  • Logger: Structured logging configuration.
  • Service: Service-specific configuration.

func GetBaseConfig

func GetBaseConfig() Config

GetBaseConfig returns a copy of the global configuration base object.

This allows safe read access to common fields like AppID, Logger, and AppSecret without exposing the global state to mutation.

Example:

cfg := config.GetBaseConfig()
fmt.Println("AppID:", cfg.AppID)

func GetSecureCopy

func GetSecureCopy() Config

GetSecureCopy returns a copy of the configuration with sensitive values masked.

It masks the base AppSecret field and any fields tagged with `sensitive:"true"` in the service configuration struct.

This is useful for logging or displaying the configuration without exposing sensitive information like secrets or passwords.

Example:

secureCfg := config.GetSecureCopy()
fmt.Printf("%+v\n", secureCfg)
Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/inovacc/config"
)

type ExampleSecureConfig struct {
	Username string `yaml:"username"`
	Password string `yaml:"password" sensitive:"true"`
}

func main() {
	dir, _ := os.MkdirTemp("", "example-secure-*")
	defer func() { _ = os.RemoveAll(dir) }()

	cfgPath := filepath.Join(dir, "config.yaml")
	_ = os.WriteFile(cfgPath, []byte(`
appID: example-app-id-12345
appSecret: example-secret-12345678
logger:
  logLevel: INFO
service:
  username: admin
  password: s3cret!
`), 0644)

	svc := &ExampleSecureConfig{}

	if err := config.InitServiceConfig(svc, cfgPath); err != nil {
		fmt.Println("error:", err)
		return
	}

	secure := config.GetSecureCopy()
	fmt.Printf("AppSecret: %s\n", secure.AppSecret)

	svcCopy, ok := secure.Service.(*ExampleSecureConfig)
	if ok {
		fmt.Printf("Username: %s\n", svcCopy.Username)
		fmt.Printf("Password: %s\n", svcCopy.Password)
	}

}
Output:
AppSecret: ********
Username: admin
Password: ********

type Logger

type Logger struct {
	LogLevel string `yaml:"logLevel" json:"logLevel" mapstructure:"logLevel"`
}

Logger defines the configuration for structured logging.

type MigrationFunc

type MigrationFunc func(data map[string]any) error

MigrationFunc transforms the configuration from one version to the next. It receives a mutable map of the raw config data and should modify it in place (e.g., rename keys, change structure).

type ValidatorFunc

type ValidatorFunc func(Config) error

ValidatorFunc is a function that validates the configuration. It receives a read-only copy of the Config and should return an error if validation fails.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL