mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-21 13:06:34 +00:00
387 lines
10 KiB
Go
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
|
|
}
|