Prepare for 1.0 release
- Fix Go version to 1.24.4 in go.mod - Add version management with version.go - Add --version flag to CLI tool - Remove deprecated LoadFromFile and LoadFromReader methods - Update tests to use new API - Create TODO.md for future improvements - Update README with Go version requirement Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
47801ce852
commit
82f09a16ec
@ -751,7 +751,7 @@ The `FILE` resolver can read any file accessible to the process:
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Go 1.18 or later
|
- Go 1.24.4 or later
|
||||||
- Valid YAML syntax in configuration files
|
- Valid YAML syntax in configuration files
|
||||||
- Appropriate credentials for cloud providers (AWS, GCP, Azure)
|
- Appropriate credentials for cloud providers (AWS, GCP, Azure)
|
||||||
- Network access for remote resolvers (Vault, Consul, etcd)
|
- Network access for remote resolvers (Vault, Consul, etcd)
|
||||||
|
74
TODO.md
Normal file
74
TODO.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# TODO for smartconfig
|
||||||
|
|
||||||
|
This file tracks improvements and features planned for future releases.
|
||||||
|
|
||||||
|
## Testing Improvements
|
||||||
|
|
||||||
|
### Concurrency Tests
|
||||||
|
- Add comprehensive concurrent access tests for Config struct
|
||||||
|
- Test race conditions with multiple goroutines accessing/modifying config
|
||||||
|
- Benchmark concurrent access patterns
|
||||||
|
|
||||||
|
### Security Tests
|
||||||
|
- Add tests for malicious input handling in EXEC resolver
|
||||||
|
- Test various command injection attempts
|
||||||
|
- Add tests for FILE resolver with symlinks and directory traversal attempts
|
||||||
|
|
||||||
|
### Performance Tests
|
||||||
|
- Add comprehensive benchmarks for all resolver types
|
||||||
|
- Benchmark large configuration files (1MB+, 10MB+, 100MB+)
|
||||||
|
- Benchmark nested interpolation performance
|
||||||
|
- Compare performance with other config libraries
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Add integration tests with mocked cloud services
|
||||||
|
- Test cloud resolver authentication failures and retry scenarios
|
||||||
|
- Test timeout behavior for all external resolvers
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
- Add godoc examples for all major functions
|
||||||
|
- Create example applications demonstrating common use cases
|
||||||
|
- Add examples for custom resolver implementation
|
||||||
|
|
||||||
|
### Guides
|
||||||
|
- Write troubleshooting guide for common issues
|
||||||
|
- Create migration guide from popular config libraries (Viper, koanf)
|
||||||
|
- Write security best practices guide
|
||||||
|
- Add performance tuning guide
|
||||||
|
|
||||||
|
## Future Features
|
||||||
|
|
||||||
|
### Caching Layer (if needed)
|
||||||
|
- Consider adding optional caching for resolver results
|
||||||
|
- Implement TTL-based cache invalidation
|
||||||
|
- Add cache statistics and monitoring
|
||||||
|
|
||||||
|
### Configuration Validation
|
||||||
|
- Add optional schema validation support
|
||||||
|
- Implement type-safe configuration structs
|
||||||
|
- Add validation rules for common patterns
|
||||||
|
|
||||||
|
### Watch/Reload Support (if needed)
|
||||||
|
- Add file watching for configuration changes
|
||||||
|
- Implement hot reload capability
|
||||||
|
- Add change notification callbacks
|
||||||
|
|
||||||
|
### Enhanced Error Handling
|
||||||
|
- Create typed errors for better error handling
|
||||||
|
- Add error context with resolution path
|
||||||
|
- Implement error aggregation for multiple failures
|
||||||
|
|
||||||
|
## Build and Release
|
||||||
|
|
||||||
|
### Release Automation
|
||||||
|
- Set up goreleaser configuration
|
||||||
|
- Automate changelog generation
|
||||||
|
- Create release binaries for multiple platforms
|
||||||
|
- Set up GitHub Actions for CI/CD
|
||||||
|
|
||||||
|
### Distribution
|
||||||
|
- Publish to common package managers (Homebrew, apt, etc.)
|
||||||
|
- Create Docker images with the CLI tool
|
||||||
|
- Set up automated security scanning
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -12,9 +13,19 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var jsonOutput bool
|
var jsonOutput bool
|
||||||
|
var showVersion bool
|
||||||
flag.BoolVar(&jsonOutput, "json", false, "Output as formatted JSON instead of YAML")
|
flag.BoolVar(&jsonOutput, "json", false, "Output as formatted JSON instead of YAML")
|
||||||
|
flag.BoolVar(&showVersion, "version", false, "Show version information")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if showVersion {
|
||||||
|
fmt.Printf("smartconfig %s\n", smartconfig.Version)
|
||||||
|
if smartconfig.GitCommit != "unknown" {
|
||||||
|
fmt.Printf("commit: %s\n", smartconfig.GitCommit)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Read from stdin
|
// Read from stdin
|
||||||
config, err := smartconfig.NewFromReader(os.Stdin)
|
config, err := smartconfig.NewFromReader(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,48 +122,6 @@ func (c *Config) RegisterResolver(name string, resolver Resolver) {
|
|||||||
c.resolvers[name] = resolver
|
c.resolvers[name] = resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFromFile loads configuration from a YAML file at the specified path.
|
|
||||||
// DEPRECATED: Use NewFromConfigPath instead for cleaner API.
|
|
||||||
func (c *Config) LoadFromFile(path string) error {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open config file: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = file.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c.LoadFromReader(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFromReader loads configuration from an io.Reader containing YAML data.
|
|
||||||
// DEPRECATED: Use NewFromReader instead for cleaner API.
|
|
||||||
func (c *Config) LoadFromReader(reader io.Reader) error {
|
|
||||||
data, err := io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse YAML first
|
|
||||||
if err := yaml.Unmarshal(data, &c.data); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse YAML: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk and interpolate the parsed structure
|
|
||||||
interpolated, err := c.walkAndInterpolate(c.data)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to interpolate config: %w", err)
|
|
||||||
}
|
|
||||||
c.data = interpolated.(map[string]interface{})
|
|
||||||
|
|
||||||
// Handle environment variable injection
|
|
||||||
if err := c.injectEnvironment(); err != nil {
|
|
||||||
return fmt.Errorf("failed to inject environment variables: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) injectEnvironment() error {
|
func (c *Config) injectEnvironment() error {
|
||||||
envData, ok := c.data["env"]
|
envData, ok := c.data["env"]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -76,9 +76,8 @@ func TestInterpolateBasic(t *testing.T) {
|
|||||||
_ = os.Setenv("BASIC", "value")
|
_ = os.Setenv("BASIC", "value")
|
||||||
defer func() { _ = os.Unsetenv("BASIC") }()
|
defer func() { _ = os.Unsetenv("BASIC") }()
|
||||||
|
|
||||||
config := New()
|
|
||||||
content := "test: ${ENV:BASIC}"
|
content := "test: ${ENV:BASIC}"
|
||||||
err := config.LoadFromReader(strings.NewReader(content))
|
config, err := NewFromReader(strings.NewReader(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to load config: %v", err)
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@ -198,9 +197,8 @@ func TestSimpleNestedInterpolation(t *testing.T) {
|
|||||||
_ = os.Unsetenv("FOO_BAR")
|
_ = os.Unsetenv("FOO_BAR")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
config := New()
|
|
||||||
content := "test: ${ENV:FOO_${ENV:SUFFIX}}"
|
content := "test: ${ENV:FOO_${ENV:SUFFIX}}"
|
||||||
err := config.LoadFromReader(strings.NewReader(content))
|
config, err := NewFromReader(strings.NewReader(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to load config: %v", err)
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@ -227,8 +225,7 @@ func TestConfigInterpolation(t *testing.T) {
|
|||||||
_ = os.Unsetenv("NESTED_VALUE")
|
_ = os.Unsetenv("NESTED_VALUE")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
config := New()
|
config, err := NewFromConfigPath("./test/config.yaml")
|
||||||
err := config.LoadFromFile("./test/config.yaml")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to load config: %v", err)
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@ -286,8 +283,6 @@ func TestConfigInterpolation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRecursionLimit(t *testing.T) {
|
func TestRecursionLimit(t *testing.T) {
|
||||||
config := New()
|
|
||||||
|
|
||||||
// Create a deeply nested interpolation that hits the recursion limit
|
// Create a deeply nested interpolation that hits the recursion limit
|
||||||
// This has 4 levels, but our max is 3
|
// This has 4 levels, but our max is 3
|
||||||
content := "value: ${ENV:LEVEL1_${ENV:LEVEL2_${ENV:LEVEL3_${ENV:LEVEL4}}}}"
|
content := "value: ${ENV:LEVEL1_${ENV:LEVEL2_${ENV:LEVEL3_${ENV:LEVEL4}}}}"
|
||||||
@ -311,7 +306,7 @@ func TestRecursionLimit(t *testing.T) {
|
|||||||
_ = os.Unsetenv("LEVEL1_depth2value")
|
_ = os.Unsetenv("LEVEL1_depth2value")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := config.LoadFromReader(strings.NewReader(content))
|
config, err := NewFromReader(strings.NewReader(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to load config: %v", err)
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@ -346,11 +341,10 @@ func TestInvalidResolverFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnknownResolver(t *testing.T) {
|
func TestUnknownResolver(t *testing.T) {
|
||||||
config := New()
|
|
||||||
content := "value: ${UNKNOWN:test}"
|
content := "value: ${UNKNOWN:test}"
|
||||||
|
|
||||||
// Unknown resolver should cause an error
|
// Unknown resolver should cause an error
|
||||||
err := config.LoadFromReader(strings.NewReader(content))
|
_, err := NewFromReader(strings.NewReader(content))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Expected error for unknown resolver, but got none")
|
t.Fatal("Expected error for unknown resolver, but got none")
|
||||||
}
|
}
|
||||||
|
7
version.go
Normal file
7
version.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package smartconfig
|
||||||
|
|
||||||
|
// Version is the current version of smartconfig
|
||||||
|
const Version = "1.0.0"
|
||||||
|
|
||||||
|
// GitCommit is the git commit hash (set at build time with -ldflags)
|
||||||
|
var GitCommit = "unknown"
|
Loading…
Reference in New Issue
Block a user