Files
Reloader/pkg/common/common.go
2025-08-29 12:24:02 +02:00

359 lines
14 KiB
Go

package common
import (
"context"
"os"
"regexp"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
type Map map[string]string
type ReloadCheckResult struct {
ShouldReload bool
AutoReload bool
}
// ReloaderOptions contains all configurable options for the Reloader controller.
// These options control how Reloader behaves when watching for changes in ConfigMaps and Secrets.
type ReloaderOptions struct {
// AutoReloadAll enables automatic reloading of all resources when their corresponding ConfigMaps/Secrets are updated
AutoReloadAll bool `json:"autoReloadAll"`
// ConfigmapUpdateOnChangeAnnotation is the annotation key used to detect changes in ConfigMaps specified by name
ConfigmapUpdateOnChangeAnnotation string `json:"configmapUpdateOnChangeAnnotation"`
// SecretUpdateOnChangeAnnotation is the annotation key used to detect changes in Secrets specified by name
SecretUpdateOnChangeAnnotation string `json:"secretUpdateOnChangeAnnotation"`
// ReloaderAutoAnnotation is the annotation key used to detect changes in any referenced ConfigMaps or Secrets
ReloaderAutoAnnotation string `json:"reloaderAutoAnnotation"`
// IgnoreResourceAnnotation is the annotation key used to ignore resources from being watched
IgnoreResourceAnnotation string `json:"ignoreResourceAnnotation"`
// ConfigmapReloaderAutoAnnotation is the annotation key used to detect changes in ConfigMaps only
ConfigmapReloaderAutoAnnotation string `json:"configmapReloaderAutoAnnotation"`
// SecretReloaderAutoAnnotation is the annotation key used to detect changes in Secrets only
SecretReloaderAutoAnnotation string `json:"secretReloaderAutoAnnotation"`
// ConfigmapExcludeReloaderAnnotation is the annotation key containing comma-separated list of ConfigMaps to exclude from watching
ConfigmapExcludeReloaderAnnotation string `json:"configmapExcludeReloaderAnnotation"`
// SecretExcludeReloaderAnnotation is the annotation key containing comma-separated list of Secrets to exclude from watching
SecretExcludeReloaderAnnotation string `json:"secretExcludeReloaderAnnotation"`
// AutoSearchAnnotation is the annotation key used to detect changes in ConfigMaps/Secrets tagged with SearchMatchAnnotation
AutoSearchAnnotation string `json:"autoSearchAnnotation"`
// SearchMatchAnnotation is the annotation key used to tag ConfigMaps/Secrets to be found by AutoSearchAnnotation
SearchMatchAnnotation string `json:"searchMatchAnnotation"`
// RolloutStrategyAnnotation is the annotation key used to define the rollout update strategy for workloads
RolloutStrategyAnnotation string `json:"rolloutStrategyAnnotation"`
// PauseDeploymentAnnotation is the annotation key used to define the time period to pause a deployment after
PauseDeploymentAnnotation string `json:"pauseDeploymentAnnotation"`
// PauseDeploymentTimeAnnotation is the annotation key used to indicate when a deployment was paused by Reloader
PauseDeploymentTimeAnnotation string `json:"pauseDeploymentTimeAnnotation"`
// LogFormat specifies the log format to use (json, or empty string for default text format)
LogFormat string `json:"logFormat"`
// LogLevel specifies the log level to use (trace, debug, info, warning, error, fatal, panic)
LogLevel string `json:"logLevel"`
// IsArgoRollouts indicates whether support for Argo Rollouts is enabled
IsArgoRollouts bool `json:"isArgoRollouts"`
// ReloadStrategy specifies the strategy used to trigger resource reloads (env-vars or annotations)
ReloadStrategy string `json:"reloadStrategy"`
// ReloadOnCreate indicates whether to trigger reloads when ConfigMaps/Secrets are created
ReloadOnCreate bool `json:"reloadOnCreate"`
// ReloadOnDelete indicates whether to trigger reloads when ConfigMaps/Secrets are deleted
ReloadOnDelete bool `json:"reloadOnDelete"`
// SyncAfterRestart indicates whether to sync add events after Reloader restarts (only works when ReloadOnCreate is true)
SyncAfterRestart bool `json:"syncAfterRestart"`
// EnableHA indicates whether High Availability mode is enabled with leader election
EnableHA bool `json:"enableHA"`
// WebhookUrl is the URL to send webhook notifications to instead of performing reloads
WebhookUrl string `json:"webhookUrl"`
// ResourcesToIgnore is a list of resource types to ignore (e.g., "configmaps" or "secrets")
ResourcesToIgnore []string `json:"resourcesToIgnore"`
// WorkloadTypesToIgnore is a list of workload types to ignore (e.g., "jobs" or "cronjobs")
WorkloadTypesToIgnore []string `json:"workloadTypesToIgnore"`
// NamespaceSelectors is a list of label selectors to filter namespaces to watch
NamespaceSelectors []string `json:"namespaceSelectors"`
// ResourceSelectors is a list of label selectors to filter ConfigMaps and Secrets to watch
ResourceSelectors []string `json:"resourceSelectors"`
// NamespacesToIgnore is a list of namespace names to ignore when watching for changes
NamespacesToIgnore []string `json:"namespacesToIgnore"`
// EnablePProf enables pprof for profiling
EnablePProf bool `json:"enablePProf"`
// PProfAddr is the address to start pprof server on
PProfAddr string `json:"pprofAddr"`
}
var CommandLineOptions *ReloaderOptions
func PublishMetaInfoConfigmap(clientset kubernetes.Interface) {
namespace := os.Getenv("RELOADER_NAMESPACE")
if namespace == "" {
logrus.Warn("RELOADER_NAMESPACE is not set, skipping meta info configmap creation")
return
}
metaInfo := &MetaInfo{
BuildInfo: *NewBuildInfo(),
ReloaderOptions: *GetCommandLineOptions(),
DeploymentInfo: metav1.ObjectMeta{
Name: os.Getenv("RELOADER_DEPLOYMENT_NAME"),
Namespace: namespace,
},
}
configMap := metaInfo.ToConfigMap()
if _, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMap.Name, metav1.GetOptions{}); err == nil {
logrus.Info("Meta info configmap already exists, updating it")
_, err = clientset.CoreV1().ConfigMaps(namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
if err != nil {
logrus.Warn("Failed to update existing meta info configmap: ", err)
}
return
}
_, err := clientset.CoreV1().ConfigMaps(namespace).Create(context.Background(), configMap, metav1.CreateOptions{})
if err != nil {
logrus.Warn("Failed to create meta info configmap: ", err)
}
}
func GetNamespaceLabelSelector(slice []string) (string, error) {
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
namespaceLabelSelector := strings.Join(slice[:], ",")
_, err := labels.Parse(namespaceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return namespaceLabelSelector, nil
}
func GetResourceLabelSelector(slice []string) (string, error) {
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
resourceLabelSelector := strings.Join(slice[:], ",")
_, err := labels.Parse(resourceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return resourceLabelSelector, nil
}
// ShouldReload checks if a resource should be reloaded based on its annotations and the provided options.
func ShouldReload(config Config, resourceType string, annotations Map, podAnnotations Map, options *ReloaderOptions) ReloadCheckResult {
// Check if this workload type should be ignored
if len(options.WorkloadTypesToIgnore) > 0 {
ignoredWorkloadTypes, err := util.GetIgnoredWorkloadTypesList()
if err != nil {
logrus.Errorf("Failed to parse ignored workload types: %v", err)
} else {
// Map Kubernetes resource types to CLI-friendly names for comparison
var resourceToCheck string
switch resourceType {
case "Job":
resourceToCheck = "jobs"
case "CronJob":
resourceToCheck = "cronjobs"
default:
resourceToCheck = resourceType // For other types, use as-is
}
// Check if current resource type should be ignored
if ignoredWorkloadTypes.Contains(resourceToCheck) {
return ReloadCheckResult{
ShouldReload: false,
}
}
}
}
ignoreResourceAnnotatonValue := config.ResourceAnnotations[options.IgnoreResourceAnnotation]
if ignoreResourceAnnotatonValue == "true" {
return ReloadCheckResult{
ShouldReload: false,
}
}
annotationValue, found := annotations[config.Annotation]
searchAnnotationValue, foundSearchAnn := annotations[options.AutoSearchAnnotation]
reloaderEnabledValue, foundAuto := annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue, foundTypedAuto := annotations[config.TypedAutoAnnotation]
excludeConfigmapAnnotationValue, foundExcludeConfigmap := annotations[options.ConfigmapExcludeReloaderAnnotation]
excludeSecretAnnotationValue, foundExcludeSecret := annotations[options.SecretExcludeReloaderAnnotation]
if !found && !foundAuto && !foundTypedAuto && !foundSearchAnn {
annotations = podAnnotations
annotationValue = annotations[config.Annotation]
searchAnnotationValue = annotations[options.AutoSearchAnnotation]
reloaderEnabledValue = annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue = annotations[config.TypedAutoAnnotation]
}
isResourceExcluded := false
switch config.Type {
case constants.ConfigmapEnvVarPostfix:
if foundExcludeConfigmap {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeConfigmapAnnotationValue)
}
case constants.SecretEnvVarPostfix:
if foundExcludeSecret {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeSecretAnnotationValue)
}
}
if isResourceExcluded {
return ReloadCheckResult{
ShouldReload: false,
}
}
reloaderEnabled, _ := strconv.ParseBool(reloaderEnabledValue)
typedAutoAnnotationEnabled, _ := strconv.ParseBool(typedAutoAnnotationEnabledValue)
if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && options.AutoReloadAll {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: true,
}
}
values := strings.Split(annotationValue, ",")
for _, value := range values {
value = strings.TrimSpace(value)
re := regexp.MustCompile("^" + value + "$")
if re.Match([]byte(config.ResourceName)) {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: false,
}
}
}
if searchAnnotationValue == "true" {
matchAnnotationValue := config.ResourceAnnotations[options.SearchMatchAnnotation]
if matchAnnotationValue == "true" {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: true,
}
}
}
return ReloadCheckResult{
ShouldReload: false,
}
}
func checkIfResourceIsExcluded(resourceName, excludedResources string) bool {
if excludedResources == "" {
return false
}
excludedResourcesList := strings.Split(excludedResources, ",")
for _, excludedResource := range excludedResourcesList {
if strings.TrimSpace(excludedResource) == resourceName {
return true
}
}
return false
}
func init() {
GetCommandLineOptions()
}
func GetCommandLineOptions() *ReloaderOptions {
if CommandLineOptions == nil {
CommandLineOptions = &ReloaderOptions{}
}
CommandLineOptions.AutoReloadAll = options.AutoReloadAll
CommandLineOptions.ConfigmapUpdateOnChangeAnnotation = options.ConfigmapUpdateOnChangeAnnotation
CommandLineOptions.SecretUpdateOnChangeAnnotation = options.SecretUpdateOnChangeAnnotation
CommandLineOptions.ReloaderAutoAnnotation = options.ReloaderAutoAnnotation
CommandLineOptions.IgnoreResourceAnnotation = options.IgnoreResourceAnnotation
CommandLineOptions.ConfigmapReloaderAutoAnnotation = options.ConfigmapReloaderAutoAnnotation
CommandLineOptions.SecretReloaderAutoAnnotation = options.SecretReloaderAutoAnnotation
CommandLineOptions.ConfigmapExcludeReloaderAnnotation = options.ConfigmapExcludeReloaderAnnotation
CommandLineOptions.SecretExcludeReloaderAnnotation = options.SecretExcludeReloaderAnnotation
CommandLineOptions.AutoSearchAnnotation = options.AutoSearchAnnotation
CommandLineOptions.SearchMatchAnnotation = options.SearchMatchAnnotation
CommandLineOptions.RolloutStrategyAnnotation = options.RolloutStrategyAnnotation
CommandLineOptions.PauseDeploymentAnnotation = options.PauseDeploymentAnnotation
CommandLineOptions.PauseDeploymentTimeAnnotation = options.PauseDeploymentTimeAnnotation
CommandLineOptions.LogFormat = options.LogFormat
CommandLineOptions.LogLevel = options.LogLevel
CommandLineOptions.ReloadStrategy = options.ReloadStrategy
CommandLineOptions.SyncAfterRestart = options.SyncAfterRestart
CommandLineOptions.EnableHA = options.EnableHA
CommandLineOptions.WebhookUrl = options.WebhookUrl
CommandLineOptions.ResourcesToIgnore = options.ResourcesToIgnore
CommandLineOptions.WorkloadTypesToIgnore = options.WorkloadTypesToIgnore
CommandLineOptions.NamespaceSelectors = options.NamespaceSelectors
CommandLineOptions.ResourceSelectors = options.ResourceSelectors
CommandLineOptions.NamespacesToIgnore = options.NamespacesToIgnore
CommandLineOptions.IsArgoRollouts = parseBool(options.IsArgoRollouts)
CommandLineOptions.ReloadOnCreate = parseBool(options.ReloadOnCreate)
CommandLineOptions.ReloadOnDelete = parseBool(options.ReloadOnDelete)
CommandLineOptions.EnablePProf = options.EnablePProf
CommandLineOptions.PProfAddr = options.PProfAddr
return CommandLineOptions
}
func parseBool(value string) bool {
if value == "" {
return false
}
result, err := strconv.ParseBool(value)
if err != nil {
return false // Default to false if parsing fails
}
return result
}