feat: Migration to new implementation

This commit is contained in:
TheiLLeniumStudios
2025-12-28 08:47:54 +01:00
parent 8b3ad89336
commit f48c5ac1b3
51 changed files with 268 additions and 13668 deletions
-11
View File
@@ -13,7 +13,6 @@ import (
)
// Hasher computes content hashes for ConfigMaps and Secrets.
// The hash is used to detect changes and trigger workload reloads.
type Hasher struct{}
// NewHasher creates a new Hasher instance.
@@ -22,7 +21,6 @@ func NewHasher() *Hasher {
}
// HashConfigMap computes a SHA1 hash of the ConfigMap's data and binaryData.
// The hash is deterministic - same content always produces the same hash.
func (h *Hasher) HashConfigMap(cm *corev1.ConfigMap) string {
if cm == nil {
return h.computeSHA("")
@@ -31,7 +29,6 @@ func (h *Hasher) HashConfigMap(cm *corev1.ConfigMap) string {
}
// HashSecret computes a SHA1 hash of the Secret's data.
// The hash is deterministic - same content always produces the same hash.
func (h *Hasher) HashSecret(secret *corev1.Secret) string {
if secret == nil {
return h.computeSHA("")
@@ -39,8 +36,6 @@ func (h *Hasher) HashSecret(secret *corev1.Secret) string {
return h.hashSecretData(secret.Data)
}
// hashConfigMapData computes a hash from ConfigMap data and binary data.
// Keys are sorted to ensure deterministic output.
func (h *Hasher) hashConfigMapData(data map[string]string, binaryData map[string][]byte) string {
values := make([]string, 0, len(data)+len(binaryData))
@@ -49,7 +44,6 @@ func (h *Hasher) hashConfigMapData(data map[string]string, binaryData map[string
}
for k, v := range binaryData {
// Binary data is base64 encoded for consistent hashing
values = append(values, k+"="+base64.StdEncoding.EncodeToString(v))
}
@@ -57,13 +51,10 @@ func (h *Hasher) hashConfigMapData(data map[string]string, binaryData map[string
return h.computeSHA(strings.Join(values, ";"))
}
// hashSecretData computes a hash from Secret data.
// Keys are sorted to ensure deterministic output.
func (h *Hasher) hashSecretData(data map[string][]byte) string {
values := make([]string, 0, len(data))
for k, v := range data {
// Secret data is stored as raw bytes, not base64 encoded
values = append(values, k+"="+string(v))
}
@@ -71,7 +62,6 @@ func (h *Hasher) hashSecretData(data map[string][]byte) string {
return h.computeSHA(strings.Join(values, ";"))
}
// computeSHA generates a SHA1 hash from a string.
func (h *Hasher) computeSHA(data string) string {
hasher := sha1.New()
_, _ = io.WriteString(hasher, data)
@@ -79,7 +69,6 @@ func (h *Hasher) computeSHA(data string) string {
}
// EmptyHash returns an empty string to signal resource deletion.
// This triggers env var removal when using the env-vars strategy.
func (h *Hasher) EmptyHash() string {
return ""
}
+6 -57
View File
@@ -19,13 +19,9 @@ const (
// MatchResult contains the result of checking if a workload should be reloaded.
type MatchResult struct {
// ShouldReload indicates whether the workload should be reloaded.
ShouldReload bool
// AutoReload indicates if this is an auto-reload (vs explicit annotation).
// This affects which container to target for env var injection.
AutoReload bool
// Reason provides a human-readable explanation of the decision.
Reason string
AutoReload bool
Reason string
}
// Matcher determines whether a workload should be reloaded based on annotations.
@@ -40,32 +36,16 @@ func NewMatcher(cfg *config.Config) *Matcher {
// MatchInput contains all the information needed to determine if a reload should occur.
type MatchInput struct {
// ResourceName is the name of the ConfigMap or Secret that changed.
ResourceName string
// ResourceNamespace is the namespace of the ConfigMap or Secret.
ResourceNamespace string
// ResourceType is whether this is a ConfigMap or Secret.
ResourceType ResourceType
// ResourceAnnotations are the annotations on the ConfigMap or Secret.
ResourceName string
ResourceNamespace string
ResourceType ResourceType
ResourceAnnotations map[string]string
// WorkloadAnnotations are the annotations on the workload (Deployment, etc.).
WorkloadAnnotations map[string]string
// PodAnnotations are the annotations on the pod template.
PodAnnotations map[string]string
PodAnnotations map[string]string
}
// ShouldReload determines if a workload should be reloaded based on its annotations.
//
// The matching logic follows this precedence (BUG FIX: explicit annotations checked first):
// 1. If the resource has the ignore annotation, skip it
// 2. If the resource is in the exclude list for this workload, skip it
// 3. If explicit reload annotation matches the resource name, reload (not auto)
// 4. If search annotation is enabled and resource has match annotation, reload (auto)
// 5. If auto annotation is "true", reload (auto)
// 6. If typed auto annotation is "true", reload (auto)
// 7. If AutoReloadAll is enabled and no explicit "false" annotations, reload (auto)
func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
// Check resource-level ignore annotation
if m.isResourceIgnored(input.ResourceAnnotations) {
return MatchResult{
ShouldReload: false,
@@ -73,10 +53,8 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// Determine which annotations to use (workload or pod template)
annotations := m.selectAnnotations(input)
// Check if resource is excluded
if m.isResourceExcluded(input.ResourceName, input.ResourceType, annotations) {
return MatchResult{
ShouldReload: false,
@@ -84,8 +62,6 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// Check explicit reload annotation (e.g., configmap.reloader.stakater.com/reload: "my-config")
// BUG FIX: Check this BEFORE auto annotations to ensure explicit references take precedence
if m.matchesExplicitAnnotation(input.ResourceName, input.ResourceType, annotations) {
return MatchResult{
ShouldReload: true,
@@ -94,7 +70,6 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// Check search/match pattern
if m.matchesSearchPattern(input.ResourceAnnotations, annotations) {
return MatchResult{
ShouldReload: true,
@@ -103,7 +78,6 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// Check auto annotations
if m.matchesAutoAnnotation(input.ResourceType, annotations) {
return MatchResult{
ShouldReload: true,
@@ -112,7 +86,6 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// Check global auto-reload-all setting
if m.matchesAutoReloadAll(input.ResourceType, annotations) {
return MatchResult{
ShouldReload: true,
@@ -127,7 +100,6 @@ func (m *Matcher) ShouldReload(input MatchInput) MatchResult {
}
}
// isResourceIgnored checks if the resource has the ignore annotation set to true.
func (m *Matcher) isResourceIgnored(resourceAnnotations map[string]string) bool {
if resourceAnnotations == nil {
return false
@@ -135,44 +107,34 @@ func (m *Matcher) isResourceIgnored(resourceAnnotations map[string]string) bool
return resourceAnnotations[m.cfg.Annotations.Ignore] == "true"
}
// selectAnnotations determines which set of annotations to use for matching.
// If workload annotations don't have relevant annotations, fall back to pod annotations.
func (m *Matcher) selectAnnotations(input MatchInput) map[string]string {
// Check if any relevant annotation exists on workload annotations
if m.hasRelevantAnnotations(input.WorkloadAnnotations, input.ResourceType) {
return input.WorkloadAnnotations
}
// Fall back to pod annotations
if m.hasRelevantAnnotations(input.PodAnnotations, input.ResourceType) {
return input.PodAnnotations
}
// Default to workload annotations even if empty
return input.WorkloadAnnotations
}
// hasRelevantAnnotations checks if the annotations contain any reload-related annotation.
func (m *Matcher) hasRelevantAnnotations(annotations map[string]string, resourceType ResourceType) bool {
if annotations == nil {
return false
}
// Check for explicit annotation
explicitAnn := m.getExplicitAnnotation(resourceType)
if _, ok := annotations[explicitAnn]; ok {
return true
}
// Check for search annotation
if _, ok := annotations[m.cfg.Annotations.Search]; ok {
return true
}
// Check for auto annotation
if _, ok := annotations[m.cfg.Annotations.Auto]; ok {
return true
}
// Check for typed auto annotation
typedAutoAnn := m.getTypedAutoAnnotation(resourceType)
if _, ok := annotations[typedAutoAnn]; ok {
return true
@@ -181,7 +143,6 @@ func (m *Matcher) hasRelevantAnnotations(annotations map[string]string, resource
return false
}
// isResourceExcluded checks if the resource is in the exclude list.
func (m *Matcher) isResourceExcluded(resourceName string, resourceType ResourceType, annotations map[string]string) bool {
if annotations == nil {
return false
@@ -209,7 +170,6 @@ func (m *Matcher) isResourceExcluded(resourceName string, resourceType ResourceT
return false
}
// matchesExplicitAnnotation checks if the resource name matches the explicit reload annotation.
func (m *Matcher) matchesExplicitAnnotation(resourceName string, resourceType ResourceType, annotations map[string]string) bool {
if annotations == nil {
return false
@@ -221,16 +181,13 @@ func (m *Matcher) matchesExplicitAnnotation(resourceName string, resourceType Re
return false
}
// Support comma-separated list of resource names with regex matching
for _, value := range strings.Split(annotationValue, ",") {
value = strings.TrimSpace(value)
if value == "" {
continue
}
// Support regex patterns
re, err := regexp.Compile("^" + value + "$")
if err != nil {
// If regex is invalid, fall back to exact match
if value == resourceName {
return true
}
@@ -244,7 +201,6 @@ func (m *Matcher) matchesExplicitAnnotation(resourceName string, resourceType Re
return false
}
// matchesSearchPattern checks if the search/match pattern is enabled.
func (m *Matcher) matchesSearchPattern(resourceAnnotations, workloadAnnotations map[string]string) bool {
if workloadAnnotations == nil || resourceAnnotations == nil {
return false
@@ -259,29 +215,24 @@ func (m *Matcher) matchesSearchPattern(resourceAnnotations, workloadAnnotations
return ok && matchValue == "true"
}
// matchesAutoAnnotation checks if auto reload is enabled via annotations.
func (m *Matcher) matchesAutoAnnotation(resourceType ResourceType, annotations map[string]string) bool {
if annotations == nil {
return false
}
// Check generic auto annotation
if annotations[m.cfg.Annotations.Auto] == "true" {
return true
}
// Check typed auto annotation
typedAutoAnn := m.getTypedAutoAnnotation(resourceType)
return annotations[typedAutoAnn] == "true"
}
// matchesAutoReloadAll checks if global auto-reload-all is enabled.
func (m *Matcher) matchesAutoReloadAll(resourceType ResourceType, annotations map[string]string) bool {
if !m.cfg.AutoReloadAll {
return false
}
// If auto annotation is explicitly set to false, don't auto-reload
if annotations != nil {
if annotations[m.cfg.Annotations.Auto] == "false" {
return false
@@ -295,7 +246,6 @@ func (m *Matcher) matchesAutoReloadAll(resourceType ResourceType, annotations ma
return true
}
// getExplicitAnnotation returns the explicit reload annotation for the resource type.
func (m *Matcher) getExplicitAnnotation(resourceType ResourceType) string {
switch resourceType {
case ResourceTypeConfigMap:
@@ -307,7 +257,6 @@ func (m *Matcher) getExplicitAnnotation(resourceType ResourceType) string {
}
}
// getTypedAutoAnnotation returns the typed auto annotation for the resource type.
func (m *Matcher) getTypedAutoAnnotation(resourceType ResourceType) string {
switch resourceType {
case ResourceTypeConfigMap:
+3 -3
View File
@@ -92,7 +92,7 @@ func TestMatcher_ShouldReload(t *testing.T) {
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"reloader.stakater.com/auto": "true",
"configmap.reloader.stakater.com/reload": "external-config",
},
PodAnnotations: nil,
@@ -277,7 +277,7 @@ func TestMatcher_ShouldReload(t *testing.T) {
ResourceType: ResourceTypeSecret,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true",
"reloader.stakater.com/auto": "true",
"secrets.exclude.reloader.stakater.com/reload": "my-secret",
},
PodAnnotations: nil,
@@ -403,7 +403,7 @@ func TestMatcher_BugFix_AutoDoesNotIgnoreExplicit(t *testing.T) {
ResourceType: ResourceTypeConfigMap,
ResourceAnnotations: nil,
WorkloadAnnotations: map[string]string{
"reloader.stakater.com/auto": "true", // Enables auto-reload
"reloader.stakater.com/auto": "true", // Enables auto-reload
"configmap.reloader.stakater.com/reload": "external-config", // Explicit list
},
PodAnnotations: nil,
+4 -4
View File
@@ -65,10 +65,10 @@ func TestPauseHandler_GetPausePeriod(t *testing.T) {
handler := NewPauseHandler(cfg)
tests := []struct {
name string
workload workload.WorkloadAccessor
wantPeriod time.Duration
wantErr bool
name string
workload workload.WorkloadAccessor
wantPeriod time.Duration
wantErr bool
}{
{
name: "valid pause period",
+24 -24
View File
@@ -128,10 +128,10 @@ func TestNamespaceFilterPredicate_Generic(t *testing.T) {
func TestLabelSelectorPredicate_Create(t *testing.T) {
tests := []struct {
name string
selector string
objectLabels map[string]string
wantAllow bool
name string
selector string
objectLabels map[string]string
wantAllow bool
}{
{
name: "match single label",
@@ -355,38 +355,38 @@ func TestCombinedFiltering(t *testing.T) {
labelPredicate := LabelSelectorPredicate(cfg)
tests := []struct {
name string
namespace string
labels map[string]string
wantNSAllow bool
name string
namespace string
labels map[string]string
wantNSAllow bool
wantLabelAllow bool
}{
{
name: "allowed namespace and matching labels",
namespace: "default",
labels: map[string]string{"managed": "true"},
wantNSAllow: true,
name: "allowed namespace and matching labels",
namespace: "default",
labels: map[string]string{"managed": "true"},
wantNSAllow: true,
wantLabelAllow: true,
},
{
name: "allowed namespace but non-matching labels",
namespace: "default",
labels: map[string]string{"managed": "false"},
wantNSAllow: true,
name: "allowed namespace but non-matching labels",
namespace: "default",
labels: map[string]string{"managed": "false"},
wantNSAllow: true,
wantLabelAllow: false,
},
{
name: "ignored namespace with matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "true"},
wantNSAllow: false,
name: "ignored namespace with matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "true"},
wantNSAllow: false,
wantLabelAllow: true,
},
{
name: "ignored namespace and non-matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "false"},
wantNSAllow: false,
name: "ignored namespace and non-matching labels",
namespace: "kube-system",
labels: map[string]string{"managed": "false"},
wantNSAllow: false,
wantLabelAllow: false,
},
}
-75
View File
@@ -3,13 +3,11 @@ package reload
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/workload"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Service orchestrates the reload logic for ConfigMaps and Secrets.
@@ -69,18 +67,15 @@ type ReloadDecision struct {
}
// ProcessConfigMap evaluates all workloads to determine which should be reloaded.
// This method does not modify any workloads - it only returns decisions.
func (s *Service) ProcessConfigMap(change ConfigMapChange, workloads []workload.WorkloadAccessor) []ReloadDecision {
if change.ConfigMap == nil {
return nil
}
// Check if we should process this event type
if !s.shouldProcessEvent(change.EventType) {
return nil
}
// Compute hash
hash := s.hasher.HashConfigMap(change.ConfigMap)
if change.EventType == EventTypeDelete {
hash = s.hasher.EmptyHash()
@@ -97,18 +92,15 @@ func (s *Service) ProcessConfigMap(change ConfigMapChange, workloads []workload.
}
// ProcessSecret evaluates all workloads to determine which should be reloaded.
// This method does not modify any workloads - it only returns decisions.
func (s *Service) ProcessSecret(change SecretChange, workloads []workload.WorkloadAccessor) []ReloadDecision {
if change.Secret == nil {
return nil
}
// Check if we should process this event type
if !s.shouldProcessEvent(change.EventType) {
return nil
}
// Compute hash
hash := s.hasher.HashSecret(change.Secret)
if change.EventType == EventTypeDelete {
hash = s.hasher.EmptyHash()
@@ -124,7 +116,6 @@ func (s *Service) ProcessSecret(change SecretChange, workloads []workload.Worklo
)
}
// processResource processes a resource change against all workloads.
func (s *Service) processResource(
resourceName string,
resourceNamespace string,
@@ -136,17 +127,14 @@ func (s *Service) processResource(
var decisions []ReloadDecision
for _, wl := range workloads {
// Skip workloads in different namespaces
if wl.GetNamespace() != resourceNamespace {
continue
}
// Check if workload should be ignored based on type
if s.cfg.IsWorkloadIgnored(string(wl.Kind())) {
continue
}
// Check if workload uses this resource (via volumes or env)
var usesResource bool
switch resourceType {
case ResourceTypeConfigMap:
@@ -155,7 +143,6 @@ func (s *Service) processResource(
usesResource = wl.UsesSecret(resourceName)
}
// Build match input
input := MatchInput{
ResourceName: resourceName,
ResourceNamespace: resourceNamespace,
@@ -165,11 +152,8 @@ func (s *Service) processResource(
PodAnnotations: wl.GetPodTemplateAnnotations(),
}
// Check if we should reload
matchResult := s.matcher.ShouldReload(input)
// For auto-reload, the workload must actually use the resource
// For explicit annotation, the user explicitly requested it
shouldReload := matchResult.ShouldReload
if matchResult.AutoReload && !usesResource {
shouldReload = false
@@ -187,7 +171,6 @@ func (s *Service) processResource(
return decisions
}
// shouldProcessEvent checks if the event type should be processed.
func (s *Service) shouldProcessEvent(eventType EventType) bool {
switch eventType {
case EventTypeCreate:
@@ -202,8 +185,6 @@ func (s *Service) shouldProcessEvent(eventType EventType) bool {
}
// ApplyReload applies the reload strategy to a workload.
// This modifies the workload in-place but does not persist the changes.
// Returns true if changes were made, false otherwise.
func (s *Service) ApplyReload(
ctx context.Context,
wl workload.WorkloadAccessor,
@@ -213,7 +194,6 @@ func (s *Service) ApplyReload(
hash string,
autoReload bool,
) (bool, error) {
// Find the target container
container := s.findTargetContainer(wl, resourceName, resourceType, autoReload)
input := StrategyInput{
@@ -226,13 +206,11 @@ func (s *Service) ApplyReload(
AutoReload: autoReload,
}
// Apply the strategy-specific changes
updated, err := s.strategy.Apply(input)
if err != nil {
return false, err
}
// Always set the attribution annotation regardless of strategy
if updated {
s.setAttributionAnnotation(wl, resourceName, resourceType, namespace, hash, container)
}
@@ -240,8 +218,6 @@ func (s *Service) ApplyReload(
return updated, nil
}
// setAttributionAnnotation sets the last-reloaded-from annotation on the pod template.
// This is always set regardless of the reload strategy for audit purposes.
func (s *Service) setAttributionAnnotation(
wl workload.WorkloadAccessor,
resourceName string,
@@ -266,16 +242,12 @@ func (s *Service) setAttributionAnnotation(
sourceJSON, err := json.Marshal(source)
if err != nil {
// Non-fatal: skip annotation if marshaling fails
return
}
wl.SetPodTemplateAnnotation(s.cfg.Annotations.LastReloadedFrom, string(sourceJSON))
}
// findTargetContainer finds the container to target for the reload.
// For auto-reload, it finds the container that uses the resource.
// For explicit annotation, it returns the first container.
func (s *Service) findTargetContainer(
wl workload.WorkloadAccessor,
resourceName string,
@@ -287,7 +259,6 @@ func (s *Service) findTargetContainer(
return nil
}
// For explicit annotation, return the first container
if !autoReload {
return &containers[0]
}
@@ -295,40 +266,31 @@ func (s *Service) findTargetContainer(
volumes := wl.GetVolumes()
initContainers := wl.GetInitContainers()
// For auto-reload, find the container that uses the resource
// Check volumes first
volumeName := s.findVolumeUsingResource(volumes, resourceName, resourceType)
if volumeName != "" {
container := s.findContainerWithVolumeMount(containers, volumeName)
if container != nil {
return container
}
// Check init containers
container = s.findContainerWithVolumeMount(initContainers, volumeName)
if container != nil {
// Return the first regular container for init container refs
return &containers[0]
}
}
// Check env references
container := s.findContainerWithEnvRef(containers, resourceName, resourceType)
if container != nil {
return container
}
// Check init container env references
container = s.findContainerWithEnvRef(initContainers, resourceName, resourceType)
if container != nil {
// Return the first regular container for init container refs
return &containers[0]
}
// Default to first container
return &containers[0]
}
// findVolumeUsingResource finds a volume that uses the given resource.
func (s *Service) findVolumeUsingResource(volumes []corev1.Volume, resourceName string, resourceType ResourceType) string {
for _, vol := range volumes {
switch resourceType {
@@ -359,7 +321,6 @@ func (s *Service) findVolumeUsingResource(volumes []corev1.Volume, resourceName
return ""
}
// findContainerWithVolumeMount finds a container that mounts the given volume.
func (s *Service) findContainerWithVolumeMount(containers []corev1.Container, volumeName string) *corev1.Container {
for i := range containers {
for _, mount := range containers[i].VolumeMounts {
@@ -371,10 +332,8 @@ func (s *Service) findContainerWithVolumeMount(containers []corev1.Container, vo
return nil
}
// findContainerWithEnvRef finds a container that references the resource via env.
func (s *Service) findContainerWithEnvRef(containers []corev1.Container, resourceName string, resourceType ResourceType) *corev1.Container {
for i := range containers {
// Check env vars
for _, env := range containers[i].Env {
if env.ValueFrom == nil {
continue
@@ -391,7 +350,6 @@ func (s *Service) findContainerWithEnvRef(containers []corev1.Container, resourc
}
}
// Check envFrom
for _, envFrom := range containers[i].EnvFrom {
switch resourceType {
case ResourceTypeConfigMap:
@@ -412,36 +370,3 @@ func (s *Service) findContainerWithEnvRef(containers []corev1.Container, resourc
func (s *Service) Hasher() *Hasher {
return s.hasher
}
// Matcher returns the matcher used by this service.
func (s *Service) Matcher() *Matcher {
return s.matcher
}
// Strategy returns the strategy used by this service.
func (s *Service) Strategy() Strategy {
return s.strategy
}
// ListWorkloads lists all workloads in the given namespace.
// If namespace is empty, lists workloads in all namespaces.
func ListWorkloads(ctx context.Context, c client.Client, namespace string, registry *workload.Registry) ([]workload.WorkloadAccessor, error) {
var workloads []workload.WorkloadAccessor
for _, kind := range registry.SupportedKinds() {
list, err := listWorkloadsByKind(ctx, c, namespace, kind)
if err != nil {
return nil, fmt.Errorf("listing %s: %w", kind, err)
}
workloads = append(workloads, list...)
}
return workloads, nil
}
// listWorkloadsByKind lists workloads of a specific kind.
func listWorkloadsByKind(ctx context.Context, c client.Client, namespace string, kind workload.Kind) ([]workload.WorkloadAccessor, error) {
// This will be implemented by the controller using the appropriate list functions
// For now, return empty slice as the controller will handle this
return nil, nil
}
+6 -36
View File
@@ -22,35 +22,22 @@ const (
// Strategy defines how workload restarts are triggered.
type Strategy interface {
// Apply applies the reload strategy to the pod spec.
// Returns true if changes were made, false otherwise.
Apply(input StrategyInput) (bool, error)
// Name returns the strategy name for logging purposes.
Name() string
}
// StrategyInput contains the information needed to apply a reload strategy.
type StrategyInput struct {
// ResourceName is the name of the ConfigMap or Secret that changed.
ResourceName string
// ResourceType is the type of resource (configmap or secret).
ResourceType ResourceType
// Namespace is the namespace of the resource.
Namespace string
// Hash is the SHA hash of the resource content.
Hash string
// Container is the container to target for env var injection.
// If nil, the first container is used.
Container *corev1.Container
// PodAnnotations is the pod template annotations map (for annotation strategy).
ResourceName string
ResourceType ResourceType
Namespace string
Hash string
Container *corev1.Container
PodAnnotations map[string]string
// AutoReload indicates if this is an auto-reload (affects container selection).
AutoReload bool
AutoReload bool
}
// ReloadSource contains metadata about what triggered a reload.
// This is stored in the annotation when using annotation strategy.
type ReloadSource struct {
Kind string `json:"kind"`
Name string `json:"name"`
@@ -61,7 +48,6 @@ type ReloadSource struct {
}
// EnvVarStrategy triggers reloads by adding/updating environment variables.
// This is the default strategy and is GitOps-friendly.
type EnvVarStrategy struct{}
// NewEnvVarStrategy creates a new EnvVarStrategy.
@@ -69,13 +55,11 @@ func NewEnvVarStrategy() *EnvVarStrategy {
return &EnvVarStrategy{}
}
// Name returns the strategy name.
func (s *EnvVarStrategy) Name() string {
return string(config.ReloadStrategyEnvVars)
}
// Apply adds, updates, or removes an environment variable to trigger a restart.
// When hash is empty (resource deleted), the env var is removed.
func (s *EnvVarStrategy) Apply(input StrategyInput) (bool, error) {
if input.Container == nil {
return false, fmt.Errorf("container is required for env-var strategy")
@@ -83,25 +67,20 @@ func (s *EnvVarStrategy) Apply(input StrategyInput) (bool, error) {
envVarName := s.envVarName(input.ResourceName, input.ResourceType)
// Handle deletion: remove the env var when hash is empty
if input.Hash == "" {
return s.removeEnvVar(input.Container, envVarName), nil
}
// Check if env var already exists
for i := range input.Container.Env {
if input.Container.Env[i].Name == envVarName {
if input.Container.Env[i].Value == input.Hash {
// Already up to date
return false, nil
}
// Update existing
input.Container.Env[i].Value = input.Hash
return true, nil
}
}
// Add new env var
input.Container.Env = append(input.Container.Env, corev1.EnvVar{
Name: envVarName,
Value: input.Hash,
@@ -110,12 +89,9 @@ func (s *EnvVarStrategy) Apply(input StrategyInput) (bool, error) {
return true, nil
}
// removeEnvVar removes an environment variable from a container.
// Returns true if a variable was removed.
func (s *EnvVarStrategy) removeEnvVar(container *corev1.Container, name string) bool {
for i := range container.Env {
if container.Env[i].Name == name {
// Remove by replacing with last element and truncating
container.Env[i] = container.Env[len(container.Env)-1]
container.Env = container.Env[:len(container.Env)-1]
return true
@@ -124,7 +100,6 @@ func (s *EnvVarStrategy) removeEnvVar(container *corev1.Container, name string)
return false
}
// envVarName generates the environment variable name for a resource.
func (s *EnvVarStrategy) envVarName(resourceName string, resourceType ResourceType) string {
var postfix string
switch resourceType {
@@ -136,8 +111,6 @@ func (s *EnvVarStrategy) envVarName(resourceName string, resourceType ResourceTy
return EnvVarPrefix + convertToEnvVarName(resourceName) + "_" + postfix
}
// convertToEnvVarName converts a string to a valid environment variable name.
// Invalid characters are replaced with underscores, and the result is uppercased.
func convertToEnvVarName(text string) string {
var buffer bytes.Buffer
upper := strings.ToUpper(text)
@@ -169,7 +142,6 @@ func NewAnnotationStrategy(cfg *config.Config) *AnnotationStrategy {
return &AnnotationStrategy{cfg: cfg}
}
// Name returns the strategy name.
func (s *AnnotationStrategy) Name() string {
return string(config.ReloadStrategyAnnotations)
}
@@ -185,7 +157,6 @@ func (s *AnnotationStrategy) Apply(input StrategyInput) (bool, error) {
containerName = input.Container.Name
}
// Create reload source metadata
source := ReloadSource{
Kind: string(input.ResourceType),
Name: input.ResourceName,
@@ -204,7 +175,6 @@ func (s *AnnotationStrategy) Apply(input StrategyInput) (bool, error) {
existingValue := input.PodAnnotations[annotationKey]
if existingValue == string(sourceJSON) {
// Already up to date
return false, nil
}