Bump Golang to 1.27.0

This commit is contained in:
Tobias Gesellchen
2026-08-23 14:50:33 +02:00
parent 18e6c32220
commit fb69ce29e0
6 changed files with 93 additions and 93 deletions
+3 -3
View File
@@ -35,7 +35,7 @@ services:
start_period: 3s
spotify-mock:
image: golang:1.26.6-alpine
image: golang:1.27.0-alpine
container_name: spotify-mock
working_dir: /app
volumes:
@@ -53,7 +53,7 @@ services:
start_period: 3s
amazon-mock:
image: golang:1.26.6-alpine
image: golang:1.27.0-alpine
container_name: amazon-mock
working_dir: /app
volumes:
@@ -71,7 +71,7 @@ services:
start_period: 3s
tunein-mock:
image: golang:1.26.6-alpine
image: golang:1.27.0-alpine
container_name: tunein-mock
working_dir: /app
volumes:
@@ -566,7 +566,7 @@ func (m *MockClient) GetNowPlaying() (*models.NowPlaying, error) {
```dockerfile
# test/docker/Dockerfile
FROM golang:1.25-alpine
FROM golang:1.27.0-alpine
WORKDIR /app
COPY . .
+84 -84
View File
@@ -115,25 +115,25 @@ type ProductionSoundTouchService struct {
type Config struct {
// Server settings
ListenAddr string `env:"LISTEN_ADDR" default:":8080"`
// SoundTouch settings
DeviceHosts []string `env:"DEVICE_HOSTS" separator:","`
DiscoveryTimeout time.Duration `env:"DISCOVERY_TIMEOUT" default:"30s"`
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT" default:"15s"`
MaxRetries int `env:"MAX_RETRIES" default:"3"`
// Connection pool
MaxConnections int `env:"MAX_CONNECTIONS" default:"10"`
IdleTimeout time.Duration `env:"IDLE_TIMEOUT" default:"5m"`
// Monitoring
MetricsEnabled bool `env:"METRICS_ENABLED" default:"true"`
HealthCheckInterval time.Duration `env:"HEALTH_CHECK_INTERVAL" default:"30s"`
// Logging
LogLevel string `env:"LOG_LEVEL" default:"info"`
LogFormat string `env:"LOG_FORMAT" default:"json"`
// Security
EnableTLS bool `env:"ENABLE_TLS" default:"false"`
TLSCertFile string `env:"TLS_CERT_FILE"`
@@ -145,7 +145,7 @@ func LoadConfig() (*Config, error) {
if err := env.Parse(cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return cfg, cfg.Validate()
}
@@ -153,15 +153,15 @@ func (c *Config) Validate() error {
if len(c.DeviceHosts) == 0 {
return fmt.Errorf("at least one device host must be specified")
}
if c.RequestTimeout < time.Second {
return fmt.Errorf("request timeout must be at least 1 second")
}
if c.EnableTLS && (c.TLSCertFile == "" || c.TLSKeyFile == "") {
return fmt.Errorf("TLS cert and key files required when TLS is enabled")
}
return nil
}
```
@@ -191,7 +191,7 @@ pool:
monitoring:
metrics_enabled: true
health_check_interval: "30s"
logging:
level: "info"
format: "json"
@@ -203,12 +203,12 @@ func LoadConfigFromFile(path string) (*Config, error) {
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, cfg.Validate()
}
```
@@ -224,14 +224,14 @@ func LoadConfigFromFile(path string) (*Config, error) {
type SecureNetworkConfig struct {
// Allowed source IP ranges
AllowedCIDRs []string
// Rate limiting
RateLimit int
RateLimitWindow time.Duration
// TLS configuration
TLSConfig *tls.Config
// Timeouts for security
ReadTimeout time.Duration
WriteTimeout time.Duration
@@ -240,7 +240,7 @@ type SecureNetworkConfig struct {
func NewSecureServer(config SecureNetworkConfig) *http.Server {
mux := http.NewServeMux()
// Add middleware
handler := applyMiddleware(mux,
corsMiddleware(),
@@ -249,7 +249,7 @@ func NewSecureServer(config SecureNetworkConfig) *http.Server {
loggingMiddleware(),
metricsMiddleware(),
)
return &http.Server{
Handler: handler,
TLSConfig: config.TLSConfig,
@@ -275,12 +275,12 @@ func (r *DeviceControlRequest) Validate() error {
if err := validate.Struct(r); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Additional business logic validation
if r.Action == "volume" && r.Volume == nil {
return fmt.Errorf("volume value required for volume action")
}
return nil
}
```
@@ -302,12 +302,12 @@ func loadSecretsFromK8s() (*SecretsConfig, error) {
if err != nil {
return nil, err
}
tlsKey, err := os.ReadFile("/etc/secrets/tls.key")
if err != nil {
return nil, err
}
return &SecretsConfig{
TLSCert: string(tlsCert),
TLSKey: string(tlsKey),
@@ -335,21 +335,21 @@ type Logger struct {
func NewLogger(level, format, component string) (*Logger, error) {
logger := logrus.New()
// Set level
logLevel, err := logrus.ParseLevel(level)
if err != nil {
return nil, err
}
logger.SetLevel(logLevel)
// Set format
if format == "json" {
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: time.RFC3339,
})
}
return &Logger{
Logger: logger,
component: component,
@@ -376,15 +376,15 @@ type Metrics struct {
RequestsTotal prometheus.CounterVec
RequestDuration prometheus.HistogramVec
RequestsInFlight prometheus.GaugeVec
// Device metrics
DevicesConnected prometheus.Gauge
DeviceHealth prometheus.GaugeVec
WebSocketConnections prometheus.Gauge
// Error metrics
ErrorsTotal prometheus.CounterVec
// Business metrics
VolumeChanges prometheus.CounterVec
SourceChanges prometheus.CounterVec
@@ -400,7 +400,7 @@ func NewMetrics() *Metrics {
},
[]string{"method", "endpoint", "status"},
),
RequestDuration: *prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "soundtouch_request_duration_seconds",
@@ -409,14 +409,14 @@ func NewMetrics() *Metrics {
},
[]string{"method", "endpoint"},
),
DevicesConnected: prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "soundtouch_devices_connected",
Help: "Number of connected devices",
},
),
DeviceHealth: *prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "soundtouch_device_health",
@@ -425,7 +425,7 @@ func NewMetrics() *Metrics {
[]string{"device_id", "device_name"},
),
}
// Register metrics
prometheus.MustRegister(
m.RequestsTotal,
@@ -433,7 +433,7 @@ func NewMetrics() *Metrics {
m.DevicesConnected,
m.DeviceHealth,
)
return m
}
@@ -457,7 +457,7 @@ type HealthChecker struct {
func (hc *HealthChecker) Start(ctx context.Context) {
ticker := time.NewTicker(hc.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
@@ -470,7 +470,7 @@ func (hc *HealthChecker) Start(ctx context.Context) {
func (hc *HealthChecker) checkAllDevices() {
var wg sync.WaitGroup
for deviceID, device := range hc.manager.devices {
wg.Add(1)
go func(id string, dev *DeviceInfo) {
@@ -478,18 +478,18 @@ func (hc *HealthChecker) checkAllDevices() {
hc.checkDevice(id, dev)
}(deviceID, device)
}
wg.Wait()
}
func (hc *HealthChecker) checkDevice(deviceID string, device *DeviceInfo) {
ctx, cancel := context.WithTimeout(context.Background(), hc.timeout)
defer cancel()
start := time.Now()
err := device.Client.Ping()
duration := time.Since(start)
if err != nil {
device.Status = DeviceStatusUnhealthy
hc.metrics.DeviceHealth.WithLabelValues(deviceID, device.Name).Set(0)
@@ -507,14 +507,14 @@ func (hc *HealthChecker) HealthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
healthy := 0
total := 0
for _, device := range hc.manager.devices {
total++
if device.Status == DeviceStatusHealthy {
healthy++
}
}
status := map[string]interface{}{
"status": "ok",
"devices": map[string]interface{}{
@@ -524,14 +524,14 @@ func (hc *HealthChecker) HealthHandler() http.HandlerFunc {
},
"timestamp": time.Now().UTC(),
}
w.Header().Set("Content-Type", "application/json")
if healthy < total {
w.WriteHeader(http.StatusServiceUnavailable)
status["status"] = "degraded"
}
json.NewEncoder(w).Encode(status)
}
}
@@ -560,16 +560,16 @@ func NewConnectionPool(maxIdle, maxActive int, idleTimeout time.Duration) *Conne
maxActive: maxActive,
idleTimeout: idleTimeout,
}
// Start cleanup goroutine
go cp.cleanup()
return cp
}
func (cp *ConnectionPool) Get(host string, port int) (*client.Client, error) {
key := fmt.Sprintf("%s:%d", host, port)
// Check if connection exists and is valid
if val, ok := cp.clients.Load(key); ok {
conn := val.(*pooledConnection)
@@ -580,35 +580,35 @@ func (cp *ConnectionPool) Get(host string, port int) (*client.Client, error) {
// Connection expired, remove it
cp.clients.Delete(key)
}
// Check active connection limit
if atomic.LoadInt64(&cp.activeCount) >= int64(cp.maxActive) {
return nil, fmt.Errorf("connection pool exhausted")
}
// Create new connection
config := client.ClientConfig{
Host: host,
Port: port,
Timeout: 15 * time.Second,
}
newClient := client.NewClient(config)
// Test connection
if err := newClient.Ping(); err != nil {
return nil, fmt.Errorf("failed to connect to %s:%d: %w", host, port, err)
}
conn := &pooledConnection{
client: newClient,
lastUsed: time.Now(),
created: time.Now(),
}
cp.clients.Store(key, conn)
atomic.AddInt64(&cp.activeCount, 1)
return newClient, nil
}
@@ -621,7 +621,7 @@ type pooledConnection struct {
func (cp *ConnectionPool) cleanup() {
ticker := time.NewTicker(cp.idleTimeout / 2)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
cp.clients.Range(func(key, val interface{}) bool {
@@ -649,10 +649,10 @@ func NewCacheManager() *CacheManager {
return &CacheManager{
// Device info rarely changes, cache for 1 hour
deviceInfoCache: cache.New(1*time.Hour, 2*time.Hour),
// Capabilities never change, cache for 24 hours
capabilitiesCache: cache.New(24*time.Hour, 48*time.Hour),
// Volume changes frequently, cache for 5 seconds
volumeCache: cache.New(5*time.Second, 10*time.Second),
}
@@ -662,12 +662,12 @@ func (cm *CacheManager) GetDeviceInfo(deviceID string, fetcher func() (*models.D
if cached, found := cm.deviceInfoCache.Get(deviceID); found {
return cached.(*models.DeviceInfo), nil
}
info, err := fetcher()
if err != nil {
return nil, err
}
cm.deviceInfoCache.Set(deviceID, info, cache.DefaultExpiration)
return info, nil
}
@@ -702,7 +702,7 @@ func NewResilientSoundTouchService(client *client.Client) *ResilientSoundTouchSe
log.Printf("Circuit breaker '%s' changed from '%s' to '%s'", name, from, to)
},
}
return &ResilientSoundTouchService{
client: client,
cb: gobreaker.NewCircuitBreaker(settings),
@@ -713,12 +713,12 @@ func (r *ResilientSoundTouchService) SetVolume(deviceID string, volume int) erro
result, err := r.cb.Execute(func() (interface{}, error) {
return nil, r.client.SetVolume(volume)
})
if err != nil {
r.metrics.ErrorsTotal.WithLabelValues("circuit_breaker", "volume").Inc()
return err
}
return result.(error)
}
```
@@ -730,16 +730,16 @@ func (app *Application) Run(ctx context.Context) error {
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start services
g, ctx := errgroup.WithContext(ctx)
// HTTP server
server := &http.Server{
Addr: app.config.ListenAddr,
Handler: app.handler,
}
g.Go(func() error {
app.logger.Info("Starting HTTP server", "addr", app.config.ListenAddr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
@@ -747,38 +747,38 @@ func (app *Application) Run(ctx context.Context) error {
}
return nil
})
// Health checker
g.Go(func() error {
return app.healthChecker.Start(ctx)
})
// WebSocket manager
g.Go(func() error {
return app.wsManager.Start(ctx)
})
// Wait for shutdown signal
go func() {
<-sigChan
app.logger.Info("Shutdown signal received")
// Graceful shutdown with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown HTTP server
if err := server.Shutdown(shutdownCtx); err != nil {
app.logger.Error("HTTP server shutdown error", "error", err)
}
// Close WebSocket connections
app.wsManager.Shutdown(shutdownCtx)
// Close connection pool
app.connectionPool.Close()
}()
return g.Wait()
}
```
@@ -791,7 +791,7 @@ func (app *Application) Run(ctx context.Context) error {
```dockerfile
# Dockerfile
FROM golang:1.25-alpine AS builder
FROM golang:1.27.0-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
@@ -830,7 +830,7 @@ services:
networks:
- soundtouch-net
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
ports:
@@ -839,7 +839,7 @@ services:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- soundtouch-net
grafana:
image: grafana/grafana:latest
ports:
@@ -1011,7 +1011,7 @@ groups:
annotations:
summary: "SoundTouch device {{ $labels.device_name }} is unhealthy"
description: "Device {{ $labels.device_id }} has been unhealthy for more than 2 minutes"
- alert: HighErrorRate
expr: rate(soundtouch_errors_total[5m]) > 0.1
for: 5m
@@ -1020,7 +1020,7 @@ groups:
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors/second over the last 5 minutes"
- alert: ServiceDown
expr: up{job="soundtouch"} == 0
for: 1m
@@ -1040,33 +1040,33 @@ func (m *Manager) BackupConfigurations() error {
Timestamp: time.Now(),
Devices: make(map[string]DeviceConfig),
}
for deviceID, device := range m.devices {
config := DeviceConfig{}
// Backup presets
if presets, err := device.Client.GetPresets(); err == nil {
config.Presets = presets
}
// Backup settings
if volume, err := device.Client.GetVolume(); err == nil {
config.Volume = volume.TargetVolume
}
if bass, err := device.Client.GetBass(); err == nil {
config.Bass = bass.TargetBass
}
backup.Devices[deviceID] = config
}
// Save to file
data, err := json.MarshalIndent(backup, "", " ")
if err != nil {
return err
}
filename := fmt.Sprintf("backup_%s.json", time.Now().Format("2006-01-02_15-04-05"))
return os.WriteFile(filepath.Join(m.config.BackupDir, filename), data, 0644)
}
@@ -1083,7 +1083,7 @@ func init() {
runtime.GOMAXPROCS(int(limit))
}
}
// Set GC target percentage
if os.Getenv("GOGC") == "" {
debug.SetGCPerc
+2 -2
View File
@@ -1,8 +1,8 @@
module navigation-station-demo
go 1.26.6
go 1.27.0
require github.com/gesellix/bose-soundtouch v0.123.0
require github.com/gesellix/bose-soundtouch v0.128.0
require github.com/gorilla/websocket v1.5.3 // indirect
+2 -2
View File
@@ -1,8 +1,8 @@
module preset-management-example
go 1.26.6
go 1.27.0
require github.com/gesellix/bose-soundtouch v0.123.0
require github.com/gesellix/bose-soundtouch v0.128.0
require github.com/gorilla/websocket v1.5.3 // indirect
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/gesellix/bose-soundtouch
go 1.26.6
go 1.27.0
require (
filippo.io/age v1.3.1