From 49409dce543571597d8725e259d1ee8041f21879 Mon Sep 17 00:00:00 2001 From: Muhammad Safwan Karim <66724151+msafwankarim@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:50:02 +0500 Subject: [PATCH] Extracted some functions to public package to reuse them in gateway (#966) * separate methods * basic refactoring * moved common code to util package to use it in gateway * common check for argo rollouts * made code compilable with latest changes on master * Moved options to separate package and created CommandLineOptions instance that will be in sync with options values. * reverted extra changes * initialize CommandLineOptions with default options in module init * wait for paused at annotation before checking deployment paused * moved things around to fix things * reverted unnecessary changes * reverted rolling_upgrade changes * reverted extra change --- Dockerfile | 6 +- internal/pkg/cmd/reloader.go | 4 +- internal/pkg/handler/upgrade.go | 173 ++++++----------- internal/pkg/handler/upgrade_test.go | 29 +++ internal/pkg/util/config.go | 2 +- internal/pkg/util/util.go | 40 ---- pkg/common/common.go | 269 +++++++++++++++++++++++++++ pkg/common/metainfo.go | 129 +++++++++++++ pkg/metainfo/metainfo.go | 234 ----------------------- 9 files changed, 486 insertions(+), 400 deletions(-) create mode 100644 pkg/common/common.go create mode 100644 pkg/common/metainfo.go delete mode 100644 pkg/metainfo/metainfo.go diff --git a/Dockerfile b/Dockerfile index 34c8941..1ba59f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,9 +34,9 @@ RUN CGO_ENABLED=0 \ GOPROXY=${GOPROXY} \ GOPRIVATE=${GOPRIVATE} \ GO111MODULE=on \ - go build -ldflags="-s -w -X github.com/stakater/Reloader/pkg/metainfo.Version=${VERSION} \ - -X github.com/stakater/Reloader/pkg/metainfo.Commit=${COMMIT} \ - -X github.com/stakater/Reloader/pkg/metainfo.BuildDate=${BUILD_DATE}" \ + go build -ldflags="-s -w -X github.com/stakater/Reloader/pkg/common.Version=${VERSION} \ + -X github.com/stakater/Reloader/pkg/common.Commit=${COMMIT} \ + -X github.com/stakater/Reloader/pkg/common.BuildDate=${BUILD_DATE}" \ -installsuffix 'static' -mod=mod -a -o manager ./ # Use distroless as minimal base image to package the manager binary diff --git a/internal/pkg/cmd/reloader.go b/internal/pkg/cmd/reloader.go index ab5284d..1402b51 100644 --- a/internal/pkg/cmd/reloader.go +++ b/internal/pkg/cmd/reloader.go @@ -19,6 +19,7 @@ import ( "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/internal/pkg/util" + "github.com/stakater/Reloader/pkg/common" "github.com/stakater/Reloader/pkg/kube" ) @@ -101,6 +102,7 @@ func getHAEnvs() (string, string) { } func startReloader(cmd *cobra.Command, args []string) { + common.GetCommandLineOptions() err := configureLogging(options.LogFormat, options.LogLevel) if err != nil { logrus.Warn(err) @@ -188,7 +190,7 @@ func startReloader(cmd *cobra.Command, args []string) { go leadership.RunLeaderElection(lock, ctx, cancel, podName, controllers) } - util.PublishMetaInfoConfigmap(clientset) + common.PublishMetaInfoConfigmap(clientset) leadership.SetupLivenessEndpoint() logrus.Fatal(http.ListenAndServe(constants.DefaultHttpListenAddr, nil)) diff --git a/internal/pkg/handler/upgrade.go b/internal/pkg/handler/upgrade.go index 67ef778..1e45c39 100644 --- a/internal/pkg/handler/upgrade.go +++ b/internal/pkg/handler/upgrade.go @@ -7,9 +7,6 @@ import ( "fmt" "io" "os" - "regexp" - "strconv" - "strings" "github.com/parnurzeal/gorequest" "github.com/prometheus/client_golang/prometheus" @@ -20,6 +17,7 @@ import ( "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/internal/pkg/util" + "github.com/stakater/Reloader/pkg/common" "github.com/stakater/Reloader/pkg/kube" app "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -264,143 +262,76 @@ func upgradeResource(clients kube.Clients, config util.Config, upgradeFuncs call return err } } + annotations := upgradeFuncs.AnnotationsFunc(resource) + podAnnotations := upgradeFuncs.PodAnnotationsFunc(resource) + result := common.ShouldReload(config, upgradeFuncs.ResourceType, annotations, podAnnotations, common.GetCommandLineOptions()) - ignoreResourceAnnotatonValue := config.ResourceAnnotations[options.IgnoreResourceAnnotation] - if ignoreResourceAnnotatonValue == "true" { + if !result.ShouldReload { + logrus.Debugf("No changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace) + return nil + } + + strategyResult := strategy(upgradeFuncs, resource, config, result.AutoReload) + + if strategyResult.Result != constants.Updated { return nil } // find correct annotation and update the resource - annotations := upgradeFuncs.AnnotationsFunc(resource) - 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] pauseInterval, foundPauseInterval := annotations[options.PauseDeploymentAnnotation] - if !found && !foundAuto && !foundTypedAuto && !foundSearchAnn { - annotations = upgradeFuncs.PodAnnotationsFunc(resource) - 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 nil - } - - strategyResult := InvokeStrategyResult{constants.NotUpdated, nil} - reloaderEnabled, _ := strconv.ParseBool(reloaderEnabledValue) - typedAutoAnnotationEnabled, _ := strconv.ParseBool(typedAutoAnnotationEnabledValue) - if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && options.AutoReloadAll { - strategyResult = strategy(upgradeFuncs, resource, config, true) - } - - if strategyResult.Result != constants.Updated && annotationValue != "" { - values := strings.Split(annotationValue, ",") - for _, value := range values { - value = strings.TrimSpace(value) - re := regexp.MustCompile("^" + value + "$") - if re.Match([]byte(config.ResourceName)) { - strategyResult = strategy(upgradeFuncs, resource, config, false) - if strategyResult.Result == constants.Updated { - break - } - } - } - } - - if strategyResult.Result != constants.Updated && searchAnnotationValue == "true" { - matchAnnotationValue := config.ResourceAnnotations[options.SearchMatchAnnotation] - if matchAnnotationValue == "true" { - strategyResult = strategy(upgradeFuncs, resource, config, true) - } - } - if strategyResult.Result == constants.Updated { - if foundPauseInterval { - deployment, ok := resource.(*app.Deployment) - if !ok { - logrus.Warnf("Annotation '%s' only applicable for deployments", options.PauseDeploymentAnnotation) - } else { - _, err = PauseDeployment(deployment, clients, config.Namespace, pauseInterval) - if err != nil { - logrus.Errorf("Failed to pause deployment '%s' in namespace '%s': %v", resourceName, config.Namespace, err) - return err - } - } - } - var err error - if upgradeFuncs.SupportsPatch && strategyResult.Patch != nil { - err = upgradeFuncs.PatchFunc(clients, config.Namespace, resource, strategyResult.Patch.Type, strategyResult.Patch.Bytes) + if foundPauseInterval { + deployment, ok := resource.(*app.Deployment) + if !ok { + logrus.Warnf("Annotation '%s' only applicable for deployments", options.PauseDeploymentAnnotation) } else { - err = upgradeFuncs.UpdateFunc(clients, config.Namespace, resource) + _, err = PauseDeployment(deployment, clients, config.Namespace, pauseInterval) + if err != nil { + logrus.Errorf("Failed to pause deployment '%s' in namespace '%s': %v", resourceName, config.Namespace, err) + return err + } } + } - if err != nil { - message := fmt.Sprintf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err) - logrus.Errorf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err) + if upgradeFuncs.SupportsPatch && strategyResult.Patch != nil { + err = upgradeFuncs.PatchFunc(clients, config.Namespace, resource, strategyResult.Patch.Type, strategyResult.Patch.Bytes) + } else { + err = upgradeFuncs.UpdateFunc(clients, config.Namespace, resource) + } - collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Inc() - collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "false", "namespace": config.Namespace}).Inc() - if recorder != nil { - recorder.Event(resource, v1.EventTypeWarning, "ReloadFail", message) - } - return err - } else { - message := fmt.Sprintf("Changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace) - message += fmt.Sprintf(", Updated '%s' of type '%s' in namespace '%s'", resourceName, upgradeFuncs.ResourceType, config.Namespace) + if err != nil { + message := fmt.Sprintf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err) + logrus.Errorf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err) - logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s'; updated '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace) + collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Inc() + collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "false", "namespace": config.Namespace}).Inc() + if recorder != nil { + recorder.Event(resource, v1.EventTypeWarning, "ReloadFail", message) + } + return err + } else { + message := fmt.Sprintf("Changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace) + message += fmt.Sprintf(", Updated '%s' of type '%s' in namespace '%s'", resourceName, upgradeFuncs.ResourceType, config.Namespace) - collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Inc() - collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": config.Namespace}).Inc() - alert_on_reload, ok := os.LookupEnv("ALERT_ON_RELOAD") - if recorder != nil { - recorder.Event(resource, v1.EventTypeNormal, "Reloaded", message) - } - if ok && alert_on_reload == "true" { - msg := fmt.Sprintf( - "Reloader detected changes in *%s* of type *%s* in namespace *%s*. Hence reloaded *%s* of type *%s* in namespace *%s*", - config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace) - alert.SendWebhookAlert(msg) - } + logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s'; updated '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace) + + collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Inc() + collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": config.Namespace}).Inc() + alert_on_reload, ok := os.LookupEnv("ALERT_ON_RELOAD") + if recorder != nil { + recorder.Event(resource, v1.EventTypeNormal, "Reloaded", message) + } + if ok && alert_on_reload == "true" { + msg := fmt.Sprintf( + "Reloader detected changes in *%s* of type *%s* in namespace *%s*. Hence reloaded *%s* of type *%s* in namespace *%s*", + config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace) + alert.SendWebhookAlert(msg) } } return nil } -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 getVolumeMountName(volumes []v1.Volume, mountType string, volumeName string) string { for i := range volumes { if mountType == constants.ConfigmapEnvVarPostfix { diff --git a/internal/pkg/handler/upgrade_test.go b/internal/pkg/handler/upgrade_test.go index 61d8aeb..891dd1f 100644 --- a/internal/pkg/handler/upgrade_test.go +++ b/internal/pkg/handler/upgrade_test.go @@ -4125,6 +4125,13 @@ func testPausingDeployment(t *testing.T, reloadStrategy string, testName string, _ = PerformAction(clients, config, deploymentFuncs, collectors, nil, invokeReloadStrategy) + // Wait for deployment to have paused-at annotation + logrus.Infof("Waiting for deployment %s to have paused-at annotation", testName) + err := waitForDeploymentPausedAtAnnotation(clients, deploymentFuncs, config.Namespace, testName, 30*time.Second) + if err != nil { + t.Errorf("Failed to wait for deployment paused-at annotation: %v", err) + } + if promtestutil.ToFloat64(collectors.Reloaded.With(labelSucceeded)) != 1 { t.Errorf("Counter was not increased") } @@ -4185,3 +4192,25 @@ func isDeploymentPaused(deployments []runtime.Object, deploymentName string) (bo } return IsPaused(deployment), nil } + +// waitForDeploymentPausedAtAnnotation waits for a deployment to have the pause-period annotation +func waitForDeploymentPausedAtAnnotation(clients kube.Clients, deploymentFuncs callbacks.RollingUpgradeFuncs, namespace, deploymentName string, timeout time.Duration) error { + start := time.Now() + + for time.Since(start) < timeout { + items := deploymentFuncs.ItemsFunc(clients, namespace) + deployment, err := FindDeploymentByName(items, deploymentName) + if err == nil { + annotations := deployment.GetAnnotations() + if annotations != nil { + if _, exists := annotations[options.PauseDeploymentTimeAnnotation]; exists { + return nil + } + } + } + + time.Sleep(100 * time.Millisecond) + } + + return fmt.Errorf("timeout waiting for deployment %s to have pause-period annotation", deploymentName) +} diff --git a/internal/pkg/util/config.go b/internal/pkg/util/config.go index 184eb68..08313ea 100644 --- a/internal/pkg/util/config.go +++ b/internal/pkg/util/config.go @@ -6,7 +6,7 @@ import ( v1 "k8s.io/api/core/v1" ) -//Config contains rolling upgrade configuration parameters +// Config contains rolling upgrade configuration parameters type Config struct { Namespace string ResourceName string diff --git a/internal/pkg/util/util.go b/internal/pkg/util/util.go index f8a5dda..096f51a 100644 --- a/internal/pkg/util/util.go +++ b/internal/pkg/util/util.go @@ -2,11 +2,9 @@ package util import ( "bytes" - "context" "encoding/base64" "errors" "fmt" - "os" "sort" "strings" @@ -15,11 +13,8 @@ import ( "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/crypto" "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/pkg/metainfo" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/kubernetes" ) // ConvertToEnvVarName converts the given text into a usable env var @@ -64,43 +59,8 @@ func GetSHAfromSecret(data map[string][]byte) string { return crypto.GenerateSHA(strings.Join(values, ";")) } -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.MetaInfo{ - BuildInfo: *metainfo.NewBuildInfo(), - ReloaderOptions: *metainfo.GetReloaderOptions(), - 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) - } -} - type List []string -type Map map[string]string - func (l *List) Contains(s string) bool { for _, v := range *l { if v == s { diff --git a/pkg/common/common.go b/pkg/common/common.go new file mode 100644 index 0000000..d6d40ba --- /dev/null +++ b/pkg/common/common.go @@ -0,0 +1,269 @@ +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/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"` + // 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"` +} + +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 ShouldReload(config util.Config, resourceType string, annotations Map, podAnnotations Map, options *ReloaderOptions) ReloadCheckResult { + + if resourceType == "Rollout" && !options.IsArgoRollouts { + 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.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) + + 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 +} diff --git a/pkg/common/metainfo.go b/pkg/common/metainfo.go new file mode 100644 index 0000000..b792c52 --- /dev/null +++ b/pkg/common/metainfo.go @@ -0,0 +1,129 @@ +package common + +import ( + "encoding/json" + "fmt" + "runtime" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Version, Commit, and BuildDate are set during the build process +// using the -X linker flag to inject these values into the binary. +// They provide metadata about the build version, commit hash, build date, and whether there are +// uncommitted changes in the source code at the time of build. +// This information is useful for debugging and tracking the specific build of the Reloader binary. +var Version = "dev" +var Commit = "unknown" +var BuildDate = "unknown" + +const ( + MetaInfoConfigmapName = "reloader-meta-info" + MetaInfoConfigmapLabelKey = "reloader.stakater.com/meta-info" + MetaInfoConfigmapLabelValue = "reloader-oss" +) + +// MetaInfo contains comprehensive metadata about the Reloader instance. +// This includes build information, configuration options, and deployment details. +type MetaInfo struct { + // BuildInfo contains information about the build version, commit, and compilation details + BuildInfo BuildInfo `json:"buildInfo"` + // ReloaderOptions contains all the configuration options and flags used by this Reloader instance + ReloaderOptions ReloaderOptions `json:"reloaderOptions"` + // DeploymentInfo contains metadata about the Kubernetes deployment of this Reloader instance + DeploymentInfo metav1.ObjectMeta `json:"deploymentInfo"` +} + +// BuildInfo contains information about the build and version of the Reloader binary. +// This includes Go version, release version, commit details, and build timestamp. +type BuildInfo struct { + // GoVersion is the version of Go used to compile the binary + GoVersion string `json:"goVersion"` + // ReleaseVersion is the version tag or branch of the Reloader release + ReleaseVersion string `json:"releaseVersion"` + // CommitHash is the Git commit hash of the source code used to build this binary + CommitHash string `json:"commitHash"` + // CommitTime is the timestamp of the Git commit used to build this binary + CommitTime time.Time `json:"commitTime"` +} + +func NewBuildInfo() *BuildInfo { + metaInfo := &BuildInfo{ + GoVersion: runtime.Version(), + ReleaseVersion: Version, + CommitHash: Commit, + CommitTime: ParseUTCTime(BuildDate), + } + + return metaInfo +} + +func (m *MetaInfo) ToConfigMap() *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: MetaInfoConfigmapName, + Namespace: m.DeploymentInfo.Namespace, + Labels: map[string]string{ + MetaInfoConfigmapLabelKey: MetaInfoConfigmapLabelValue, + }, + }, + Data: map[string]string{ + "buildInfo": toJson(m.BuildInfo), + "reloaderOptions": toJson(m.ReloaderOptions), + "deploymentInfo": toJson(m.DeploymentInfo), + }, + } +} + +func NewMetaInfo(configmap *v1.ConfigMap) (*MetaInfo, error) { + var buildInfo BuildInfo + if val, ok := configmap.Data["buildInfo"]; ok { + err := json.Unmarshal([]byte(val), &buildInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal buildInfo: %w", err) + } + } + + var reloaderOptions ReloaderOptions + if val, ok := configmap.Data["reloaderOptions"]; ok { + err := json.Unmarshal([]byte(val), &reloaderOptions) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal reloaderOptions: %w", err) + } + } + + var deploymentInfo metav1.ObjectMeta + if val, ok := configmap.Data["deploymentInfo"]; ok { + err := json.Unmarshal([]byte(val), &deploymentInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal deploymentInfo: %w", err) + } + } + + return &MetaInfo{ + BuildInfo: buildInfo, + ReloaderOptions: reloaderOptions, + DeploymentInfo: deploymentInfo, + }, nil +} + +func toJson(data interface{}) string { + jsonData, err := json.Marshal(data) + if err != nil { + return "" + } + return string(jsonData) +} + +func ParseUTCTime(value string) time.Time { + if value == "" { + return time.Time{} // Return zero time if value is empty + } + t, err := time.Parse(time.RFC3339, value) + if err != nil { + return time.Time{} // Return zero time if parsing fails + } + return t +} diff --git a/pkg/metainfo/metainfo.go b/pkg/metainfo/metainfo.go deleted file mode 100644 index 8cfad3c..0000000 --- a/pkg/metainfo/metainfo.go +++ /dev/null @@ -1,234 +0,0 @@ -package metainfo - -import ( - "encoding/json" - "fmt" - "runtime" - "strconv" - "time" - - "github.com/stakater/Reloader/internal/pkg/options" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Version, Commit, and BuildDate are set during the build process -// using the -X linker flag to inject these values into the binary. -// They provide metadata about the build version, commit hash, build date, and whether there are -// uncommitted changes in the source code at the time of build. -// This information is useful for debugging and tracking the specific build of the Reloader binary. -var Version = "dev" -var Commit = "unknown" -var BuildDate = "unknown" - -const ( - MetaInfoConfigmapName = "reloader-meta-info" - MetaInfoConfigmapLabelKey = "reloader.stakater.com/meta-info" - MetaInfoConfigmapLabelValue = "reloader-oss" -) - -// 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"` - // 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"` -} - -// MetaInfo contains comprehensive metadata about the Reloader instance. -// This includes build information, configuration options, and deployment details. -type MetaInfo struct { - // BuildInfo contains information about the build version, commit, and compilation details - BuildInfo BuildInfo `json:"buildInfo"` - // ReloaderOptions contains all the configuration options and flags used by this Reloader instance - ReloaderOptions ReloaderOptions `json:"reloaderOptions"` - // DeploymentInfo contains metadata about the Kubernetes deployment of this Reloader instance - DeploymentInfo metav1.ObjectMeta `json:"deploymentInfo"` -} - -func GetReloaderOptions() *ReloaderOptions { - return &ReloaderOptions{ - AutoReloadAll: options.AutoReloadAll, - ConfigmapUpdateOnChangeAnnotation: options.ConfigmapUpdateOnChangeAnnotation, - SecretUpdateOnChangeAnnotation: options.SecretUpdateOnChangeAnnotation, - ReloaderAutoAnnotation: options.ReloaderAutoAnnotation, - IgnoreResourceAnnotation: options.IgnoreResourceAnnotation, - ConfigmapReloaderAutoAnnotation: options.ConfigmapReloaderAutoAnnotation, - SecretReloaderAutoAnnotation: options.SecretReloaderAutoAnnotation, - ConfigmapExcludeReloaderAnnotation: options.ConfigmapExcludeReloaderAnnotation, - SecretExcludeReloaderAnnotation: options.SecretExcludeReloaderAnnotation, - AutoSearchAnnotation: options.AutoSearchAnnotation, - SearchMatchAnnotation: options.SearchMatchAnnotation, - RolloutStrategyAnnotation: options.RolloutStrategyAnnotation, - PauseDeploymentAnnotation: options.PauseDeploymentAnnotation, - PauseDeploymentTimeAnnotation: options.PauseDeploymentTimeAnnotation, - LogFormat: options.LogFormat, - LogLevel: options.LogLevel, - IsArgoRollouts: parseBool(options.IsArgoRollouts), - ReloadStrategy: options.ReloadStrategy, - ReloadOnCreate: parseBool(options.ReloadOnCreate), - ReloadOnDelete: parseBool(options.ReloadOnDelete), - SyncAfterRestart: options.SyncAfterRestart, - EnableHA: options.EnableHA, - WebhookUrl: options.WebhookUrl, - ResourcesToIgnore: options.ResourcesToIgnore, - NamespaceSelectors: options.NamespaceSelectors, - ResourceSelectors: options.ResourceSelectors, - NamespacesToIgnore: options.NamespacesToIgnore, - } -} - -// BuildInfo contains information about the build and version of the Reloader binary. -// This includes Go version, release version, commit details, and build timestamp. -type BuildInfo struct { - // GoVersion is the version of Go used to compile the binary - GoVersion string `json:"goVersion"` - // ReleaseVersion is the version tag or branch of the Reloader release - ReleaseVersion string `json:"releaseVersion"` - // CommitHash is the Git commit hash of the source code used to build this binary - CommitHash string `json:"commitHash"` - // CommitTime is the timestamp of the Git commit used to build this binary - CommitTime time.Time `json:"commitTime"` -} - -func NewBuildInfo() *BuildInfo { - metaInfo := &BuildInfo{ - GoVersion: runtime.Version(), - ReleaseVersion: Version, - CommitHash: Commit, - CommitTime: ParseUTCTime(BuildDate), - } - - return metaInfo -} - -func (m *MetaInfo) ToConfigMap() *v1.ConfigMap { - return &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: MetaInfoConfigmapName, - Namespace: m.DeploymentInfo.Namespace, - Labels: map[string]string{ - MetaInfoConfigmapLabelKey: MetaInfoConfigmapLabelValue, - }, - }, - Data: map[string]string{ - "buildInfo": toJson(m.BuildInfo), - "reloaderOptions": toJson(m.ReloaderOptions), - "deploymentInfo": toJson(m.DeploymentInfo), - }, - } -} - -func NewMetaInfo(configmap *v1.ConfigMap) (*MetaInfo, error) { - var buildInfo BuildInfo - if val, ok := configmap.Data["buildInfo"]; ok { - err := json.Unmarshal([]byte(val), &buildInfo) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal buildInfo: %w", err) - } - } - - var reloaderOptions ReloaderOptions - if val, ok := configmap.Data["reloaderOptions"]; ok { - err := json.Unmarshal([]byte(val), &reloaderOptions) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal reloaderOptions: %w", err) - } - } - - var deploymentInfo metav1.ObjectMeta - if val, ok := configmap.Data["deploymentInfo"]; ok { - err := json.Unmarshal([]byte(val), &deploymentInfo) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal deploymentInfo: %w", err) - } - } - - return &MetaInfo{ - BuildInfo: buildInfo, - ReloaderOptions: reloaderOptions, - DeploymentInfo: deploymentInfo, - }, nil -} - -func toJson(data interface{}) string { - jsonData, err := json.Marshal(data) - if err != nil { - return "" - } - return string(jsonData) -} - -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 -} - -func ParseUTCTime(value string) time.Time { - if value == "" { - return time.Time{} // Return zero time if value is empty - } - t, err := time.Parse(time.RFC3339, value) - if err != nil { - return time.Time{} // Return zero time if parsing fails - } - return t -}