Files
Reloader/internal/pkg/reload/service.go
T

387 lines
10 KiB
Go

package reload
import (
"context"
"encoding/json"
"time"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/workload"
corev1 "k8s.io/api/core/v1"
)
// Service orchestrates the reload logic for ConfigMaps and Secrets.
type Service struct {
cfg *config.Config
hasher *Hasher
matcher *Matcher
strategy Strategy
}
// NewService creates a new reload Service with the given configuration.
func NewService(cfg *config.Config) *Service {
return &Service{
cfg: cfg,
hasher: NewHasher(),
matcher: NewMatcher(cfg),
strategy: NewStrategy(cfg),
}
}
// ResourceChange represents a change event for a ConfigMap or Secret.
type ResourceChange interface {
IsNil() bool
GetEventType() EventType
GetName() string
GetNamespace() string
GetAnnotations() map[string]string
GetResourceType() ResourceType
ComputeHash(hasher *Hasher) string
}
// ConfigMapChange represents a change event for a ConfigMap.
type ConfigMapChange struct {
ConfigMap *corev1.ConfigMap
EventType EventType
}
func (c ConfigMapChange) IsNil() bool { return c.ConfigMap == nil }
func (c ConfigMapChange) GetEventType() EventType { return c.EventType }
func (c ConfigMapChange) GetName() string { return c.ConfigMap.Name }
func (c ConfigMapChange) GetNamespace() string { return c.ConfigMap.Namespace }
func (c ConfigMapChange) GetAnnotations() map[string]string { return c.ConfigMap.Annotations }
func (c ConfigMapChange) GetResourceType() ResourceType { return ResourceTypeConfigMap }
func (c ConfigMapChange) ComputeHash(h *Hasher) string { return h.HashConfigMap(c.ConfigMap) }
// SecretChange represents a change event for a Secret.
type SecretChange struct {
Secret *corev1.Secret
EventType EventType
}
func (c SecretChange) IsNil() bool { return c.Secret == nil }
func (c SecretChange) GetEventType() EventType { return c.EventType }
func (c SecretChange) GetName() string { return c.Secret.Name }
func (c SecretChange) GetNamespace() string { return c.Secret.Namespace }
func (c SecretChange) GetAnnotations() map[string]string { return c.Secret.Annotations }
func (c SecretChange) GetResourceType() ResourceType { return ResourceTypeSecret }
func (c SecretChange) ComputeHash(h *Hasher) string { return h.HashSecret(c.Secret) }
// EventType represents the type of change event.
type EventType string
const (
// EventTypeCreate indicates a resource was created.
EventTypeCreate EventType = "create"
// EventTypeUpdate indicates a resource was updated.
EventTypeUpdate EventType = "update"
// EventTypeDelete indicates a resource was deleted.
EventTypeDelete EventType = "delete"
)
// ReloadDecision contains the result of evaluating whether to reload a workload.
type ReloadDecision struct {
// Workload is the workload accessor.
Workload workload.WorkloadAccessor
// ShouldReload indicates whether the workload should be reloaded.
ShouldReload bool
// AutoReload indicates if this is an auto-reload.
AutoReload bool
// Reason provides a human-readable explanation.
Reason string
// Hash is the computed hash of the resource content.
Hash string
}
// FilterDecisions returns only decisions where ShouldReload is true.
func FilterDecisions(decisions []ReloadDecision) []ReloadDecision {
var result []ReloadDecision
for _, d := range decisions {
if d.ShouldReload {
result = append(result, d)
}
}
return result
}
// Process evaluates all workloads to determine which should be reloaded.
func (s *Service) Process(change ResourceChange, workloads []workload.WorkloadAccessor) []ReloadDecision {
if change.IsNil() {
return nil
}
if !s.shouldProcessEvent(change.GetEventType()) {
return nil
}
hash := change.ComputeHash(s.hasher)
if change.GetEventType() == EventTypeDelete {
hash = s.hasher.EmptyHash()
}
return s.processResource(
change.GetName(),
change.GetNamespace(),
change.GetAnnotations(),
change.GetResourceType(),
hash,
workloads,
)
}
func (s *Service) processResource(
resourceName string,
resourceNamespace string,
resourceAnnotations map[string]string,
resourceType ResourceType,
hash string,
workloads []workload.WorkloadAccessor,
) []ReloadDecision {
var decisions []ReloadDecision
for _, wl := range workloads {
if wl.GetNamespace() != resourceNamespace {
continue
}
if s.cfg.IsWorkloadIgnored(string(wl.Kind())) {
continue
}
var usesResource bool
switch resourceType {
case ResourceTypeConfigMap:
usesResource = wl.UsesConfigMap(resourceName)
case ResourceTypeSecret:
usesResource = wl.UsesSecret(resourceName)
}
input := MatchInput{
ResourceName: resourceName,
ResourceNamespace: resourceNamespace,
ResourceType: resourceType,
ResourceAnnotations: resourceAnnotations,
WorkloadAnnotations: wl.GetAnnotations(),
PodAnnotations: wl.GetPodTemplateAnnotations(),
}
matchResult := s.matcher.ShouldReload(input)
shouldReload := matchResult.ShouldReload
if matchResult.AutoReload && !usesResource {
shouldReload = false
}
decisions = append(decisions, ReloadDecision{
Workload: wl,
ShouldReload: shouldReload,
AutoReload: matchResult.AutoReload,
Reason: matchResult.Reason,
Hash: hash,
})
}
return decisions
}
func (s *Service) shouldProcessEvent(eventType EventType) bool {
switch eventType {
case EventTypeCreate:
return s.cfg.ReloadOnCreate
case EventTypeDelete:
return s.cfg.ReloadOnDelete
case EventTypeUpdate:
return true
default:
return false
}
}
// ApplyReload applies the reload strategy to a workload.
func (s *Service) ApplyReload(
ctx context.Context,
wl workload.WorkloadAccessor,
resourceName string,
resourceType ResourceType,
namespace string,
hash string,
autoReload bool,
) (bool, error) {
container := s.findTargetContainer(wl, resourceName, resourceType, autoReload)
input := StrategyInput{
ResourceName: resourceName,
ResourceType: resourceType,
Namespace: namespace,
Hash: hash,
Container: container,
PodAnnotations: wl.GetPodTemplateAnnotations(),
AutoReload: autoReload,
}
updated, err := s.strategy.Apply(input)
if err != nil {
return false, err
}
if updated {
s.setAttributionAnnotation(wl, resourceName, resourceType, namespace, hash, container)
}
return updated, nil
}
func (s *Service) setAttributionAnnotation(
wl workload.WorkloadAccessor,
resourceName string,
resourceType ResourceType,
namespace string,
hash string,
container *corev1.Container,
) {
containerName := ""
if container != nil {
containerName = container.Name
}
source := ReloadSource{
Kind: string(resourceType),
Name: resourceName,
Namespace: namespace,
Hash: hash,
Containers: []string{containerName},
ReloadedAt: time.Now().UTC(),
}
sourceJSON, err := json.Marshal(source)
if err != nil {
return
}
wl.SetPodTemplateAnnotation(s.cfg.Annotations.LastReloadedFrom, string(sourceJSON))
}
func (s *Service) findTargetContainer(
wl workload.WorkloadAccessor,
resourceName string,
resourceType ResourceType,
autoReload bool,
) *corev1.Container {
containers := wl.GetContainers()
if len(containers) == 0 {
return nil
}
if !autoReload {
return &containers[0]
}
volumes := wl.GetVolumes()
initContainers := wl.GetInitContainers()
volumeName := s.findVolumeUsingResource(volumes, resourceName, resourceType)
if volumeName != "" {
container := s.findContainerWithVolumeMount(containers, volumeName)
if container != nil {
return container
}
container = s.findContainerWithVolumeMount(initContainers, volumeName)
if container != nil {
return &containers[0]
}
}
container := s.findContainerWithEnvRef(containers, resourceName, resourceType)
if container != nil {
return container
}
container = s.findContainerWithEnvRef(initContainers, resourceName, resourceType)
if container != nil {
return &containers[0]
}
return &containers[0]
}
func (s *Service) findVolumeUsingResource(volumes []corev1.Volume, resourceName string, resourceType ResourceType) string {
for _, vol := range volumes {
switch resourceType {
case ResourceTypeConfigMap:
if vol.ConfigMap != nil && vol.ConfigMap.Name == resourceName {
return vol.Name
}
if vol.Projected != nil {
for _, src := range vol.Projected.Sources {
if src.ConfigMap != nil && src.ConfigMap.Name == resourceName {
return vol.Name
}
}
}
case ResourceTypeSecret:
if vol.Secret != nil && vol.Secret.SecretName == resourceName {
return vol.Name
}
if vol.Projected != nil {
for _, src := range vol.Projected.Sources {
if src.Secret != nil && src.Secret.Name == resourceName {
return vol.Name
}
}
}
}
}
return ""
}
func (s *Service) findContainerWithVolumeMount(containers []corev1.Container, volumeName string) *corev1.Container {
for i := range containers {
for _, mount := range containers[i].VolumeMounts {
if mount.Name == volumeName {
return &containers[i]
}
}
}
return nil
}
func (s *Service) findContainerWithEnvRef(containers []corev1.Container, resourceName string, resourceType ResourceType) *corev1.Container {
for i := range containers {
for _, env := range containers[i].Env {
if env.ValueFrom == nil {
continue
}
switch resourceType {
case ResourceTypeConfigMap:
if env.ValueFrom.ConfigMapKeyRef != nil && env.ValueFrom.ConfigMapKeyRef.Name == resourceName {
return &containers[i]
}
case ResourceTypeSecret:
if env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name == resourceName {
return &containers[i]
}
}
}
for _, envFrom := range containers[i].EnvFrom {
switch resourceType {
case ResourceTypeConfigMap:
if envFrom.ConfigMapRef != nil && envFrom.ConfigMapRef.Name == resourceName {
return &containers[i]
}
case ResourceTypeSecret:
if envFrom.SecretRef != nil && envFrom.SecretRef.Name == resourceName {
return &containers[i]
}
}
}
}
return nil
}
// Hasher returns the hasher used by this service.
func (s *Service) Hasher() *Hasher {
return s.hasher
}