Files
Reloader/internal/pkg/reload/strategy.go
T
2026-06-22 20:34:40 +05:00

206 lines
5.5 KiB
Go

package reload
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"github.com/stakater/Reloader/internal/pkg/config"
)
const (
// EnvVarPrefix is the prefix for environment variables added by Reloader.
EnvVarPrefix = "STAKATER_"
// ConfigmapEnvVarPostfix is the postfix for ConfigMap environment variables.
ConfigmapEnvVarPostfix = "CONFIGMAP"
// SecretEnvVarPostfix is the postfix for Secret environment variables.
SecretEnvVarPostfix = "SECRET"
// SecretProviderClassEnvVarPostfix is the postfix for SecretProviderClass environment variables.
SecretProviderClassEnvVarPostfix = "SECRETPROVIDERCLASS"
)
// Strategy defines how workload restarts are triggered.
type Strategy interface {
Apply(input StrategyInput) (bool, error)
Name() string
}
// StrategyInput contains the information needed to apply a reload strategy.
type StrategyInput struct {
ResourceName string
ResourceType ResourceType
Namespace string
Hash string
Container *corev1.Container
PodAnnotations map[string]string
AutoReload bool
}
// ReloadSource contains metadata about what triggered a reload.
type ReloadSource struct {
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Hash string `json:"hash"`
Containers []string `json:"containers"`
ReloadedAt time.Time `json:"reloadedAt"`
}
// EnvVarStrategy triggers reloads by adding/updating environment variables.
type EnvVarStrategy struct{}
// NewEnvVarStrategy creates a new EnvVarStrategy.
func NewEnvVarStrategy() *EnvVarStrategy {
return &EnvVarStrategy{}
}
func (s *EnvVarStrategy) Name() string {
return string(config.ReloadStrategyEnvVars)
}
// Apply adds, updates, or removes an environment variable to trigger a restart.
func (s *EnvVarStrategy) Apply(input StrategyInput) (bool, error) {
if input.Container == nil {
return false, fmt.Errorf("container is required for env-var strategy")
}
envVarName := s.envVarName(input.ResourceName, input.ResourceType)
if input.Hash == "" {
return s.removeEnvVar(input.Container, envVarName), nil
}
for i := range input.Container.Env {
if input.Container.Env[i].Name == envVarName {
if input.Container.Env[i].Value == input.Hash {
return false, nil
}
input.Container.Env[i].Value = input.Hash
return true, nil
}
}
input.Container.Env = append(input.Container.Env, corev1.EnvVar{
Name: envVarName,
Value: input.Hash,
})
return true, nil
}
func (s *EnvVarStrategy) removeEnvVar(container *corev1.Container, name string) bool {
for i := range container.Env {
if container.Env[i].Name == name {
container.Env[i] = container.Env[len(container.Env)-1]
container.Env = container.Env[:len(container.Env)-1]
return true
}
}
return false
}
func (s *EnvVarStrategy) envVarName(resourceName string, resourceType ResourceType) string {
var postfix string
switch resourceType {
case ResourceTypeConfigMap:
postfix = ConfigmapEnvVarPostfix
case ResourceTypeSecret:
postfix = SecretEnvVarPostfix
case ResourceTypeSecretProviderClass:
postfix = SecretProviderClassEnvVarPostfix
}
return EnvVarPrefix + convertToEnvVarName(resourceName) + "_" + postfix
}
func convertToEnvVarName(text string) string {
var buffer bytes.Buffer
upper := strings.ToUpper(text)
lastCharValid := false
for i := 0; i < len(upper); i++ {
ch := upper[i]
if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
buffer.WriteByte(ch)
lastCharValid = true
} else {
if lastCharValid {
buffer.WriteByte('_')
}
lastCharValid = false
}
}
return buffer.String()
}
// AnnotationStrategy triggers reloads by adding/updating pod template annotations.
type AnnotationStrategy struct {
cfg *config.Config
}
// NewAnnotationStrategy creates a new AnnotationStrategy.
func NewAnnotationStrategy(cfg *config.Config) *AnnotationStrategy {
return &AnnotationStrategy{cfg: cfg}
}
func (s *AnnotationStrategy) Name() string {
return string(config.ReloadStrategyAnnotations)
}
// Apply adds or updates a pod annotation to trigger a restart.
func (s *AnnotationStrategy) Apply(input StrategyInput) (bool, error) {
if input.PodAnnotations == nil {
return false, fmt.Errorf("pod annotations map is required for annotation strategy")
}
containerName := ""
if input.Container != nil {
containerName = input.Container.Name
}
source := ReloadSource{
Kind: string(input.ResourceType),
Name: input.ResourceName,
Namespace: input.Namespace,
Hash: input.Hash,
Containers: []string{containerName},
ReloadedAt: time.Now().UTC(),
}
sourceJSON, err := json.Marshal(source)
if err != nil {
return false, fmt.Errorf("failed to marshal reload source: %w", err)
}
annotationKey := s.cfg.Annotations.LastReloadedFrom
existingValue := input.PodAnnotations[annotationKey]
// Idempotent on kind+name+hash, ignoring ReloadedAt: a timestamped compare
// would force a rollout every reconcile (one CSI rotation fans out to N
// SecretProviderClassPodStatus updates → N rollouts).
if existingValue != "" {
var prev ReloadSource
if err := json.Unmarshal([]byte(existingValue), &prev); err == nil &&
prev.Kind == source.Kind && prev.Name == source.Name && prev.Hash == source.Hash {
return false, nil
}
}
input.PodAnnotations[annotationKey] = string(sourceJSON)
return true, nil
}
// NewStrategy creates a Strategy based on the configuration.
func NewStrategy(cfg *config.Config) Strategy {
switch cfg.ReloadStrategy {
case config.ReloadStrategyAnnotations:
return NewAnnotationStrategy(cfg)
default:
return NewEnvVarStrategy()
}
}