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
-154
View File
@@ -1,154 +0,0 @@
package alert
import (
"fmt"
"os"
"strings"
"github.com/parnurzeal/gorequest"
"github.com/sirupsen/logrus"
)
type AlertSink string
const (
AlertSinkSlack AlertSink = "slack"
AlertSinkTeams AlertSink = "teams"
AlertSinkGoogleChat AlertSink = "gchat"
AlertSinkRaw AlertSink = "raw"
)
// function to send alert msg to webhook service
func SendWebhookAlert(msg string) {
webhook_url, ok := os.LookupEnv("ALERT_WEBHOOK_URL")
if !ok {
logrus.Error("ALERT_WEBHOOK_URL env variable not provided")
return
}
webhook_url = strings.TrimSpace(webhook_url)
alert_sink := os.Getenv("ALERT_SINK")
alert_sink = strings.ToLower(strings.TrimSpace(alert_sink))
// Provision to add Proxy to reach webhook server if required
webhook_proxy := os.Getenv("ALERT_WEBHOOK_PROXY")
webhook_proxy = strings.TrimSpace(webhook_proxy)
// Provision to add Additional information in the alert. e.g ClusterName
alert_additional_info, ok := os.LookupEnv("ALERT_ADDITIONAL_INFO")
if ok {
alert_additional_info = strings.TrimSpace(alert_additional_info)
msg = fmt.Sprintf("%s : %s", alert_additional_info, msg)
}
switch AlertSink(alert_sink) {
case AlertSinkSlack:
sendSlackAlert(webhook_url, webhook_proxy, msg)
case AlertSinkTeams:
sendTeamsAlert(webhook_url, webhook_proxy, msg)
case AlertSinkGoogleChat:
sendGoogleChatAlert(webhook_url, webhook_proxy, msg)
default:
msg = strings.ReplaceAll(msg, "*", "")
sendRawWebhookAlert(webhook_url, webhook_proxy, msg)
}
}
// function to handle server redirection
func redirectPolicy(req gorequest.Request, via []gorequest.Request) error {
return fmt.Errorf("incorrect token (redirection)")
}
// function to send alert to slack
func sendSlackAlert(webhookUrl string, proxy string, msg string) []error {
attachment := Attachment{
Text: msg,
Color: "good",
AuthorName: "Reloader",
}
payload := WebhookMessage{
Attachments: []Attachment{attachment},
}
request := gorequest.New().Proxy(proxy)
resp, _, err := request.
Post(webhookUrl).
RedirectPolicy(redirectPolicy).
Send(payload).
End()
if err != nil {
return err
}
if resp.StatusCode >= 400 {
return []error{fmt.Errorf("error sending msg. status: %v", resp.Status)}
}
return nil
}
// function to send alert to Microsoft Teams webhook
func sendTeamsAlert(webhookUrl string, proxy string, msg string) []error {
attachment := Attachment{
Text: msg,
}
request := gorequest.New().Proxy(proxy)
resp, _, err := request.
Post(webhookUrl).
RedirectPolicy(redirectPolicy).
Send(attachment).
End()
if err != nil {
return err
}
if resp.StatusCode != 200 {
return []error{fmt.Errorf("error sending msg. status: %v", resp.Status)}
}
return nil
}
// function to send alert to Google Chat webhook
func sendGoogleChatAlert(webhookUrl string, proxy string, msg string) []error {
payload := map[string]interface{}{
"text": msg,
}
request := gorequest.New().Proxy(proxy)
resp, _, err := request.
Post(webhookUrl).
RedirectPolicy(redirectPolicy).
Send(payload).
End()
if err != nil {
return err
}
if resp.StatusCode != 200 {
return []error{fmt.Errorf("error sending msg. status: %v", resp.Status)}
}
return nil
}
// function to send alert to webhook service as text
func sendRawWebhookAlert(webhookUrl string, proxy string, msg string) []error {
request := gorequest.New().Proxy(proxy)
resp, _, err := request.
Post(webhookUrl).
Type("text").
RedirectPolicy(redirectPolicy).
Send(msg).
End()
if err != nil {
return err
}
if resp.StatusCode >= 400 {
return []error{fmt.Errorf("error sending msg. status: %v", resp.Status)}
}
return nil
}
-61
View File
@@ -1,61 +0,0 @@
package alert
type WebhookMessage struct {
Username string `json:"username,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
IconURL string `json:"icon_url,omitempty"`
Channel string `json:"channel,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Parse string `json:"parse,omitempty"`
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original,omitempty"`
DeleteOriginal bool `json:"delete_original,omitempty"`
ReplyBroadcast bool `json:"reply_broadcast,omitempty"`
}
type Attachment struct {
Color string `json:"color,omitempty"`
Fallback string `json:"fallback,omitempty"`
CallbackID string `json:"callback_id,omitempty"`
ID int `json:"id,omitempty"`
AuthorID string `json:"author_id,omitempty"`
AuthorName string `json:"author_name,omitempty"`
AuthorSubname string `json:"author_subname,omitempty"`
AuthorLink string `json:"author_link,omitempty"`
AuthorIcon string `json:"author_icon,omitempty"`
Title string `json:"title,omitempty"`
TitleLink string `json:"title_link,omitempty"`
Pretext string `json:"pretext,omitempty"`
Text string `json:"text,omitempty"`
ImageURL string `json:"image_url,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"`
ServiceName string `json:"service_name,omitempty"`
ServiceIcon string `json:"service_icon,omitempty"`
FromURL string `json:"from_url,omitempty"`
OriginalURL string `json:"original_url,omitempty"`
MarkdownIn []string `json:"mrkdwn_in,omitempty"`
Footer string `json:"footer,omitempty"`
FooterIcon string `json:"footer_icon,omitempty"`
}
type Field struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
type Action struct {
Type string `json:"type"`
Text string `json:"text"`
Url string `json:"url"`
Style string `json:"style"`
}
-9
View File
@@ -1,9 +0,0 @@
package app
import "github.com/stakater/Reloader/internal/pkg/cmd"
// Run runs the command
func Run() error {
cmd := cmd.NewReloaderCommand()
return cmd.Execute()
}
-579
View File
@@ -1,579 +0,0 @@
package callbacks
import (
"context"
"errors"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
patchtypes "k8s.io/apimachinery/pkg/types"
"maps"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
)
// ItemFunc is a generic function to return a specific resource in given namespace
type ItemFunc func(kube.Clients, string, string) (runtime.Object, error)
// ItemsFunc is a generic function to return a specific resource array in given namespace
type ItemsFunc func(kube.Clients, string) []runtime.Object
// ContainersFunc is a generic func to return containers
type ContainersFunc func(runtime.Object) []v1.Container
// InitContainersFunc is a generic func to return containers
type InitContainersFunc func(runtime.Object) []v1.Container
// VolumesFunc is a generic func to return volumes
type VolumesFunc func(runtime.Object) []v1.Volume
// UpdateFunc performs the resource update
type UpdateFunc func(kube.Clients, string, runtime.Object) error
// PatchFunc performs the resource patch
type PatchFunc func(kube.Clients, string, runtime.Object, patchtypes.PatchType, []byte) error
// PatchTemplateFunc is a generic func to return strategic merge JSON patch template
type PatchTemplatesFunc func() PatchTemplates
// AnnotationsFunc is a generic func to return annotations
type AnnotationsFunc func(runtime.Object) map[string]string
// PodAnnotationsFunc is a generic func to return annotations
type PodAnnotationsFunc func(runtime.Object) map[string]string
// RollingUpgradeFuncs contains generic functions to perform rolling upgrade
type RollingUpgradeFuncs struct {
ItemFunc ItemFunc
ItemsFunc ItemsFunc
AnnotationsFunc AnnotationsFunc
PodAnnotationsFunc PodAnnotationsFunc
ContainersFunc ContainersFunc
ContainerPatchPathFunc ContainersFunc
InitContainersFunc InitContainersFunc
UpdateFunc UpdateFunc
PatchFunc PatchFunc
PatchTemplatesFunc PatchTemplatesFunc
VolumesFunc VolumesFunc
ResourceType string
SupportsPatch bool
}
// PatchTemplates contains merge JSON patch templates
type PatchTemplates struct {
AnnotationTemplate string
EnvVarTemplate string
DeleteEnvVarTemplate string
}
// GetDeploymentItem returns the deployment in given namespace
func GetDeploymentItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
deployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get deployment %v", err)
return nil, err
}
if deployment.Spec.Template.Annotations == nil {
annotations := make(map[string]string)
deployment.Spec.Template.Annotations = annotations
}
return deployment, nil
}
// GetDeploymentItems returns the deployments in given namespace
func GetDeploymentItems(clients kube.Clients, namespace string) []runtime.Object {
deployments, err := clients.KubernetesClient.AppsV1().Deployments(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list deployments %v", err)
}
items := make([]runtime.Object, len(deployments.Items))
// Ensure we always have pod annotations to add to
for i, v := range deployments.Items {
if v.Spec.Template.Annotations == nil {
annotations := make(map[string]string)
deployments.Items[i].Spec.Template.Annotations = annotations
}
items[i] = &deployments.Items[i]
}
return items
}
// GetCronJobItem returns the job in given namespace
func GetCronJobItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
cronjob, err := clients.KubernetesClient.BatchV1().CronJobs(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get cronjob %v", err)
return nil, err
}
return cronjob, nil
}
// GetCronJobItems returns the jobs in given namespace
func GetCronJobItems(clients kube.Clients, namespace string) []runtime.Object {
cronjobs, err := clients.KubernetesClient.BatchV1().CronJobs(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list cronjobs %v", err)
}
items := make([]runtime.Object, len(cronjobs.Items))
// Ensure we always have pod annotations to add to
for i, v := range cronjobs.Items {
if v.Spec.JobTemplate.Spec.Template.Annotations == nil {
annotations := make(map[string]string)
cronjobs.Items[i].Spec.JobTemplate.Spec.Template.Annotations = annotations
}
items[i] = &cronjobs.Items[i]
}
return items
}
// GetJobItem returns the job in given namespace
func GetJobItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
job, err := clients.KubernetesClient.BatchV1().Jobs(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get job %v", err)
return nil, err
}
return job, nil
}
// GetJobItems returns the jobs in given namespace
func GetJobItems(clients kube.Clients, namespace string) []runtime.Object {
jobs, err := clients.KubernetesClient.BatchV1().Jobs(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list jobs %v", err)
}
items := make([]runtime.Object, len(jobs.Items))
// Ensure we always have pod annotations to add to
for i, v := range jobs.Items {
if v.Spec.Template.Annotations == nil {
annotations := make(map[string]string)
jobs.Items[i].Spec.Template.Annotations = annotations
}
items[i] = &jobs.Items[i]
}
return items
}
// GetDaemonSetItem returns the daemonSet in given namespace
func GetDaemonSetItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
daemonSet, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get daemonSet %v", err)
return nil, err
}
return daemonSet, nil
}
// GetDaemonSetItems returns the daemonSets in given namespace
func GetDaemonSetItems(clients kube.Clients, namespace string) []runtime.Object {
daemonSets, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list daemonSets %v", err)
}
items := make([]runtime.Object, len(daemonSets.Items))
// Ensure we always have pod annotations to add to
for i, v := range daemonSets.Items {
if v.Spec.Template.Annotations == nil {
daemonSets.Items[i].Spec.Template.Annotations = make(map[string]string)
}
items[i] = &daemonSets.Items[i]
}
return items
}
// GetStatefulSetItem returns the statefulSet in given namespace
func GetStatefulSetItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
statefulSet, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get statefulSet %v", err)
return nil, err
}
return statefulSet, nil
}
// GetStatefulSetItems returns the statefulSets in given namespace
func GetStatefulSetItems(clients kube.Clients, namespace string) []runtime.Object {
statefulSets, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list statefulSets %v", err)
}
items := make([]runtime.Object, len(statefulSets.Items))
// Ensure we always have pod annotations to add to
for i, v := range statefulSets.Items {
if v.Spec.Template.Annotations == nil {
statefulSets.Items[i].Spec.Template.Annotations = make(map[string]string)
}
items[i] = &statefulSets.Items[i]
}
return items
}
// GetRolloutItem returns the rollout in given namespace
func GetRolloutItem(clients kube.Clients, name string, namespace string) (runtime.Object, error) {
rollout, err := clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).Get(context.TODO(), name, meta_v1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get Rollout %v", err)
return nil, err
}
return rollout, nil
}
// GetRolloutItems returns the rollouts in given namespace
func GetRolloutItems(clients kube.Clients, namespace string) []runtime.Object {
rollouts, err := clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
logrus.Errorf("Failed to list Rollouts %v", err)
}
items := make([]runtime.Object, len(rollouts.Items))
// Ensure we always have pod annotations to add to
for i, v := range rollouts.Items {
if v.Spec.Template.Annotations == nil {
rollouts.Items[i].Spec.Template.Annotations = make(map[string]string)
}
items[i] = &rollouts.Items[i]
}
return items
}
// GetDeploymentAnnotations returns the annotations of given deployment
func GetDeploymentAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.Deployment).Annotations == nil {
item.(*appsv1.Deployment).Annotations = make(map[string]string)
}
return item.(*appsv1.Deployment).Annotations
}
// GetCronJobAnnotations returns the annotations of given cronjob
func GetCronJobAnnotations(item runtime.Object) map[string]string {
if item.(*batchv1.CronJob).Annotations == nil {
item.(*batchv1.CronJob).Annotations = make(map[string]string)
}
return item.(*batchv1.CronJob).Annotations
}
// GetJobAnnotations returns the annotations of given job
func GetJobAnnotations(item runtime.Object) map[string]string {
if item.(*batchv1.Job).Annotations == nil {
item.(*batchv1.Job).Annotations = make(map[string]string)
}
return item.(*batchv1.Job).Annotations
}
// GetDaemonSetAnnotations returns the annotations of given daemonSet
func GetDaemonSetAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.DaemonSet).Annotations == nil {
item.(*appsv1.DaemonSet).Annotations = make(map[string]string)
}
return item.(*appsv1.DaemonSet).Annotations
}
// GetStatefulSetAnnotations returns the annotations of given statefulSet
func GetStatefulSetAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.StatefulSet).Annotations == nil {
item.(*appsv1.StatefulSet).Annotations = make(map[string]string)
}
return item.(*appsv1.StatefulSet).Annotations
}
// GetRolloutAnnotations returns the annotations of given rollout
func GetRolloutAnnotations(item runtime.Object) map[string]string {
if item.(*argorolloutv1alpha1.Rollout).Annotations == nil {
item.(*argorolloutv1alpha1.Rollout).Annotations = make(map[string]string)
}
return item.(*argorolloutv1alpha1.Rollout).Annotations
}
// GetDeploymentPodAnnotations returns the pod's annotations of given deployment
func GetDeploymentPodAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.Deployment).Spec.Template.Annotations == nil {
item.(*appsv1.Deployment).Spec.Template.Annotations = make(map[string]string)
}
return item.(*appsv1.Deployment).Spec.Template.Annotations
}
// GetCronJobPodAnnotations returns the pod's annotations of given cronjob
func GetCronJobPodAnnotations(item runtime.Object) map[string]string {
if item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations == nil {
item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations = make(map[string]string)
}
return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations
}
// GetJobPodAnnotations returns the pod's annotations of given job
func GetJobPodAnnotations(item runtime.Object) map[string]string {
if item.(*batchv1.Job).Spec.Template.Annotations == nil {
item.(*batchv1.Job).Spec.Template.Annotations = make(map[string]string)
}
return item.(*batchv1.Job).Spec.Template.Annotations
}
// GetDaemonSetPodAnnotations returns the pod's annotations of given daemonSet
func GetDaemonSetPodAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.DaemonSet).Spec.Template.Annotations == nil {
item.(*appsv1.DaemonSet).Spec.Template.Annotations = make(map[string]string)
}
return item.(*appsv1.DaemonSet).Spec.Template.Annotations
}
// GetStatefulSetPodAnnotations returns the pod's annotations of given statefulSet
func GetStatefulSetPodAnnotations(item runtime.Object) map[string]string {
if item.(*appsv1.StatefulSet).Spec.Template.Annotations == nil {
item.(*appsv1.StatefulSet).Spec.Template.Annotations = make(map[string]string)
}
return item.(*appsv1.StatefulSet).Spec.Template.Annotations
}
// GetRolloutPodAnnotations returns the pod's annotations of given rollout
func GetRolloutPodAnnotations(item runtime.Object) map[string]string {
if item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations == nil {
item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations = make(map[string]string)
}
return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations
}
// GetDeploymentContainers returns the containers of given deployment
func GetDeploymentContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.Deployment).Spec.Template.Spec.Containers
}
// GetCronJobContainers returns the containers of given cronjob
func GetCronJobContainers(item runtime.Object) []v1.Container {
return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Containers
}
// GetJobContainers returns the containers of given job
func GetJobContainers(item runtime.Object) []v1.Container {
return item.(*batchv1.Job).Spec.Template.Spec.Containers
}
// GetDaemonSetContainers returns the containers of given daemonSet
func GetDaemonSetContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.DaemonSet).Spec.Template.Spec.Containers
}
// GetStatefulSetContainers returns the containers of given statefulSet
func GetStatefulSetContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.StatefulSet).Spec.Template.Spec.Containers
}
// GetRolloutContainers returns the containers of given rollout
func GetRolloutContainers(item runtime.Object) []v1.Container {
return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.Containers
}
// GetDeploymentInitContainers returns the containers of given deployment
func GetDeploymentInitContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.Deployment).Spec.Template.Spec.InitContainers
}
// GetCronJobInitContainers returns the containers of given cronjob
func GetCronJobInitContainers(item runtime.Object) []v1.Container {
return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.InitContainers
}
// GetJobInitContainers returns the containers of given job
func GetJobInitContainers(item runtime.Object) []v1.Container {
return item.(*batchv1.Job).Spec.Template.Spec.InitContainers
}
// GetDaemonSetInitContainers returns the containers of given daemonSet
func GetDaemonSetInitContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.DaemonSet).Spec.Template.Spec.InitContainers
}
// GetStatefulSetInitContainers returns the containers of given statefulSet
func GetStatefulSetInitContainers(item runtime.Object) []v1.Container {
return item.(*appsv1.StatefulSet).Spec.Template.Spec.InitContainers
}
// GetRolloutInitContainers returns the containers of given rollout
func GetRolloutInitContainers(item runtime.Object) []v1.Container {
return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.InitContainers
}
// GetPatchTemplates returns patch templates
func GetPatchTemplates() PatchTemplates {
return PatchTemplates{
AnnotationTemplate: `{"spec":{"template":{"metadata":{"annotations":{"%s":"%s"}}}}}`, // strategic merge patch
EnvVarTemplate: `{"spec":{"template":{"spec":{"containers":[{"name":"%s","env":[{"name":"%s","value":"%s"}]}]}}}}`, // strategic merge patch
DeleteEnvVarTemplate: `[{"op":"remove","path":"/spec/template/spec/containers/%d/env/%d"}]`, // JSON patch
}
}
// UpdateDeployment performs rolling upgrade on deployment
func UpdateDeployment(clients kube.Clients, namespace string, resource runtime.Object) error {
deployment := resource.(*appsv1.Deployment)
_, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, meta_v1.UpdateOptions{FieldManager: "Reloader"})
return err
}
// PatchDeployment performs rolling upgrade on deployment
func PatchDeployment(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
deployment := resource.(*appsv1.Deployment)
_, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Patch(context.TODO(), deployment.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"})
return err
}
// CreateJobFromCronjob performs rolling upgrade on cronjob
func CreateJobFromCronjob(clients kube.Clients, namespace string, resource runtime.Object) error {
cronJob := resource.(*batchv1.CronJob)
annotations := make(map[string]string)
annotations["cronjob.kubernetes.io/instantiate"] = "manual"
maps.Copy(annotations, cronJob.Spec.JobTemplate.Annotations)
job := &batchv1.Job{
ObjectMeta: meta_v1.ObjectMeta{
GenerateName: cronJob.Name + "-",
Namespace: cronJob.Namespace,
Annotations: annotations,
Labels: cronJob.Spec.JobTemplate.Labels,
OwnerReferences: []meta_v1.OwnerReference{*meta_v1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
},
Spec: cronJob.Spec.JobTemplate.Spec,
}
_, err := clients.KubernetesClient.BatchV1().Jobs(namespace).Create(context.TODO(), job, meta_v1.CreateOptions{FieldManager: "Reloader"})
return err
}
func PatchCronJob(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
return errors.New("not supported patching: CronJob")
}
// ReCreateJobFromjob performs rolling upgrade on job
func ReCreateJobFromjob(clients kube.Clients, namespace string, resource runtime.Object) error {
oldJob := resource.(*batchv1.Job)
job := oldJob.DeepCopy()
// Delete the old job
policy := meta_v1.DeletePropagationBackground
err := clients.KubernetesClient.BatchV1().Jobs(namespace).Delete(context.TODO(), job.Name, meta_v1.DeleteOptions{PropagationPolicy: &policy})
if err != nil {
return err
}
// Remove fields that should not be specified when creating a new Job
job.ResourceVersion = ""
job.UID = ""
job.CreationTimestamp = meta_v1.Time{}
job.Status = batchv1.JobStatus{}
// Remove problematic labels
delete(job.Spec.Template.Labels, "controller-uid")
delete(job.Spec.Template.Labels, batchv1.ControllerUidLabel)
delete(job.Spec.Template.Labels, batchv1.JobNameLabel)
delete(job.Spec.Template.Labels, "job-name")
// Remove the selector to allow it to be auto-generated
job.Spec.Selector = nil
// Create the new job with same spec
_, err = clients.KubernetesClient.BatchV1().Jobs(namespace).Create(context.TODO(), job, meta_v1.CreateOptions{FieldManager: "Reloader"})
return err
}
func PatchJob(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
return errors.New("not supported patching: Job")
}
// UpdateDaemonSet performs rolling upgrade on daemonSet
func UpdateDaemonSet(clients kube.Clients, namespace string, resource runtime.Object) error {
daemonSet := resource.(*appsv1.DaemonSet)
_, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).Update(context.TODO(), daemonSet, meta_v1.UpdateOptions{FieldManager: "Reloader"})
return err
}
func PatchDaemonSet(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
daemonSet := resource.(*appsv1.DaemonSet)
_, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).Patch(context.TODO(), daemonSet.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"})
return err
}
// UpdateStatefulSet performs rolling upgrade on statefulSet
func UpdateStatefulSet(clients kube.Clients, namespace string, resource runtime.Object) error {
statefulSet := resource.(*appsv1.StatefulSet)
_, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).Update(context.TODO(), statefulSet, meta_v1.UpdateOptions{FieldManager: "Reloader"})
return err
}
func PatchStatefulSet(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
statefulSet := resource.(*appsv1.StatefulSet)
_, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).Patch(context.TODO(), statefulSet.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"})
return err
}
// UpdateRollout performs rolling upgrade on rollout
func UpdateRollout(clients kube.Clients, namespace string, resource runtime.Object) error {
rollout := resource.(*argorolloutv1alpha1.Rollout)
strategy := rollout.GetAnnotations()[options.RolloutStrategyAnnotation]
var err error
switch options.ToArgoRolloutStrategy(strategy) {
case options.RestartStrategy:
_, err = clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).Patch(context.TODO(), rollout.Name, patchtypes.MergePatchType, []byte(fmt.Sprintf(`{"spec": {"restartAt": "%s"}}`, time.Now().Format(time.RFC3339))), meta_v1.PatchOptions{FieldManager: "Reloader"})
case options.RolloutStrategy:
_, err = clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).Update(context.TODO(), rollout, meta_v1.UpdateOptions{FieldManager: "Reloader"})
}
return err
}
func PatchRollout(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
return errors.New("not supported patching: Rollout")
}
// GetDeploymentVolumes returns the Volumes of given deployment
func GetDeploymentVolumes(item runtime.Object) []v1.Volume {
return item.(*appsv1.Deployment).Spec.Template.Spec.Volumes
}
// GetCronJobVolumes returns the Volumes of given cronjob
func GetCronJobVolumes(item runtime.Object) []v1.Volume {
return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Volumes
}
// GetJobVolumes returns the Volumes of given job
func GetJobVolumes(item runtime.Object) []v1.Volume {
return item.(*batchv1.Job).Spec.Template.Spec.Volumes
}
// GetDaemonSetVolumes returns the Volumes of given daemonSet
func GetDaemonSetVolumes(item runtime.Object) []v1.Volume {
return item.(*appsv1.DaemonSet).Spec.Template.Spec.Volumes
}
// GetStatefulSetVolumes returns the Volumes of given statefulSet
func GetStatefulSetVolumes(item runtime.Object) []v1.Volume {
return item.(*appsv1.StatefulSet).Spec.Template.Spec.Volumes
}
// GetRolloutVolumes returns the Volumes of given rollout
func GetRolloutVolumes(item runtime.Object) []v1.Volume {
return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.Volumes
}
@@ -1,773 +0,0 @@
package callbacks_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
fakeargoclientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake"
patchtypes "k8s.io/apimachinery/pkg/types"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/pkg/kube"
)
var (
clients = setupTestClients()
)
type testFixtures struct {
defaultContainers []v1.Container
defaultInitContainers []v1.Container
defaultVolumes []v1.Volume
namespace string
}
func newTestFixtures() testFixtures {
return testFixtures{
defaultContainers: []v1.Container{{Name: "container1"}, {Name: "container2"}},
defaultInitContainers: []v1.Container{{Name: "init-container1"}, {Name: "init-container2"}},
defaultVolumes: []v1.Volume{{Name: "volume1"}, {Name: "volume2"}},
namespace: "default",
}
}
func setupTestClients() kube.Clients {
return kube.Clients{
KubernetesClient: fake.NewSimpleClientset(),
ArgoRolloutClient: fakeargoclientset.NewSimpleClientset(),
}
}
// TestUpdateRollout test update rollout strategy annotation
func TestUpdateRollout(t *testing.T) {
namespace := "test-ns"
cases := map[string]struct {
name string
strategy string
isRestart bool
}{
"test-without-strategy": {
name: "defaults to rollout strategy",
strategy: "",
isRestart: false,
},
"test-with-restart-strategy": {
name: "triggers a restart strategy",
strategy: "restart",
isRestart: true,
},
"test-with-rollout-strategy": {
name: "triggers a rollout strategy",
strategy: "rollout",
isRestart: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rollout, err := testutil.CreateRollout(
clients.ArgoRolloutClient, name, namespace,
map[string]string{options.RolloutStrategyAnnotation: tc.strategy},
)
if err != nil {
t.Errorf("creating rollout: %v", err)
}
modifiedChan := watchRollout(rollout.Name, namespace)
err = callbacks.UpdateRollout(clients, namespace, rollout)
if err != nil {
t.Errorf("updating rollout: %v", err)
}
rollout, err = clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(
namespace).Get(context.TODO(), rollout.Name, metav1.GetOptions{})
if err != nil {
t.Errorf("getting rollout: %v", err)
}
if isRestartStrategy(rollout) == tc.isRestart {
t.Errorf("Should not be a restart strategy")
}
select {
case <-modifiedChan:
// object has been modified
case <-time.After(1 * time.Second):
t.Errorf("Rollout has not been updated")
}
})
}
}
func TestPatchRollout(t *testing.T) {
namespace := "test-ns"
rollout := testutil.GetRollout(namespace, "test", map[string]string{options.RolloutStrategyAnnotation: ""})
err := callbacks.PatchRollout(clients, namespace, rollout, patchtypes.StrategicMergePatchType, []byte(`{"spec": {}}`))
assert.EqualError(t, err, "not supported patching: Rollout")
}
func TestResourceItem(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
createFunc func(kube.Clients, string, string) (runtime.Object, error)
getItemFunc func(kube.Clients, string, string) (runtime.Object, error)
deleteFunc func(kube.Clients, string, string) error
}{
{
name: "Deployment",
createFunc: createTestDeploymentWithAnnotations,
getItemFunc: callbacks.GetDeploymentItem,
deleteFunc: deleteTestDeployment,
},
{
name: "CronJob",
createFunc: createTestCronJobWithAnnotations,
getItemFunc: callbacks.GetCronJobItem,
deleteFunc: deleteTestCronJob,
},
{
name: "Job",
createFunc: createTestJobWithAnnotations,
getItemFunc: callbacks.GetJobItem,
deleteFunc: deleteTestJob,
},
{
name: "DaemonSet",
createFunc: createTestDaemonSetWithAnnotations,
getItemFunc: callbacks.GetDaemonSetItem,
deleteFunc: deleteTestDaemonSet,
},
{
name: "StatefulSet",
createFunc: createTestStatefulSetWithAnnotations,
getItemFunc: callbacks.GetStatefulSetItem,
deleteFunc: deleteTestStatefulSet,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource, err := tt.createFunc(clients, fixtures.namespace, "1")
assert.NoError(t, err)
accessor, err := meta.Accessor(resource)
assert.NoError(t, err)
_, err = tt.getItemFunc(clients, accessor.GetName(), fixtures.namespace)
assert.NoError(t, err)
err = tt.deleteFunc(clients, fixtures.namespace, accessor.GetName())
assert.NoError(t, err)
})
}
}
func TestResourceItems(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
createFunc func(kube.Clients, string) error
getItemsFunc func(kube.Clients, string) []runtime.Object
deleteFunc func(kube.Clients, string) error
expectedCount int
}{
{
name: "Deployments",
createFunc: createTestDeployments,
getItemsFunc: callbacks.GetDeploymentItems,
deleteFunc: deleteTestDeployments,
expectedCount: 2,
},
{
name: "CronJobs",
createFunc: createTestCronJobs,
getItemsFunc: callbacks.GetCronJobItems,
deleteFunc: deleteTestCronJobs,
expectedCount: 2,
},
{
name: "Jobs",
createFunc: createTestJobs,
getItemsFunc: callbacks.GetJobItems,
deleteFunc: deleteTestJobs,
expectedCount: 2,
},
{
name: "DaemonSets",
createFunc: createTestDaemonSets,
getItemsFunc: callbacks.GetDaemonSetItems,
deleteFunc: deleteTestDaemonSets,
expectedCount: 2,
},
{
name: "StatefulSets",
createFunc: createTestStatefulSets,
getItemsFunc: callbacks.GetStatefulSetItems,
deleteFunc: deleteTestStatefulSets,
expectedCount: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.createFunc(clients, fixtures.namespace)
assert.NoError(t, err)
items := tt.getItemsFunc(clients, fixtures.namespace)
assert.Equal(t, tt.expectedCount, len(items))
})
}
}
func TestGetAnnotations(t *testing.T) {
testAnnotations := map[string]string{"version": "1"}
tests := []struct {
name string
resource runtime.Object
getFunc func(runtime.Object) map[string]string
}{
{"Deployment", &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetDeploymentAnnotations},
{"CronJob", &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetCronJobAnnotations},
{"Job", &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetJobAnnotations},
{"DaemonSet", &appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetDaemonSetAnnotations},
{"StatefulSet", &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetStatefulSetAnnotations},
{"Rollout", &argorolloutv1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Annotations: testAnnotations}}, callbacks.GetRolloutAnnotations},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, testAnnotations, tt.getFunc(tt.resource))
})
}
}
func TestGetPodAnnotations(t *testing.T) {
testAnnotations := map[string]string{"version": "1"}
tests := []struct {
name string
resource runtime.Object
getFunc func(runtime.Object) map[string]string
}{
{"Deployment", createResourceWithPodAnnotations(&appsv1.Deployment{}, testAnnotations), callbacks.GetDeploymentPodAnnotations},
{"CronJob", createResourceWithPodAnnotations(&batchv1.CronJob{}, testAnnotations), callbacks.GetCronJobPodAnnotations},
{"Job", createResourceWithPodAnnotations(&batchv1.Job{}, testAnnotations), callbacks.GetJobPodAnnotations},
{"DaemonSet", createResourceWithPodAnnotations(&appsv1.DaemonSet{}, testAnnotations), callbacks.GetDaemonSetPodAnnotations},
{"StatefulSet", createResourceWithPodAnnotations(&appsv1.StatefulSet{}, testAnnotations), callbacks.GetStatefulSetPodAnnotations},
{"Rollout", createResourceWithPodAnnotations(&argorolloutv1alpha1.Rollout{}, testAnnotations), callbacks.GetRolloutPodAnnotations},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, testAnnotations, tt.getFunc(tt.resource))
})
}
}
func TestGetContainers(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
resource runtime.Object
getFunc func(runtime.Object) []v1.Container
}{
{"Deployment", createResourceWithContainers(&appsv1.Deployment{}, fixtures.defaultContainers), callbacks.GetDeploymentContainers},
{"DaemonSet", createResourceWithContainers(&appsv1.DaemonSet{}, fixtures.defaultContainers), callbacks.GetDaemonSetContainers},
{"StatefulSet", createResourceWithContainers(&appsv1.StatefulSet{}, fixtures.defaultContainers), callbacks.GetStatefulSetContainers},
{"CronJob", createResourceWithContainers(&batchv1.CronJob{}, fixtures.defaultContainers), callbacks.GetCronJobContainers},
{"Job", createResourceWithContainers(&batchv1.Job{}, fixtures.defaultContainers), callbacks.GetJobContainers},
{"Rollout", createResourceWithContainers(&argorolloutv1alpha1.Rollout{}, fixtures.defaultContainers), callbacks.GetRolloutContainers},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, fixtures.defaultContainers, tt.getFunc(tt.resource))
})
}
}
func TestGetInitContainers(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
resource runtime.Object
getFunc func(runtime.Object) []v1.Container
}{
{"Deployment", createResourceWithInitContainers(&appsv1.Deployment{}, fixtures.defaultInitContainers), callbacks.GetDeploymentInitContainers},
{"DaemonSet", createResourceWithInitContainers(&appsv1.DaemonSet{}, fixtures.defaultInitContainers), callbacks.GetDaemonSetInitContainers},
{"StatefulSet", createResourceWithInitContainers(&appsv1.StatefulSet{}, fixtures.defaultInitContainers), callbacks.GetStatefulSetInitContainers},
{"CronJob", createResourceWithInitContainers(&batchv1.CronJob{}, fixtures.defaultInitContainers), callbacks.GetCronJobInitContainers},
{"Job", createResourceWithInitContainers(&batchv1.Job{}, fixtures.defaultInitContainers), callbacks.GetJobInitContainers},
{"Rollout", createResourceWithInitContainers(&argorolloutv1alpha1.Rollout{}, fixtures.defaultInitContainers), callbacks.GetRolloutInitContainers},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, fixtures.defaultInitContainers, tt.getFunc(tt.resource))
})
}
}
func TestUpdateResources(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
createFunc func(kube.Clients, string, string) (runtime.Object, error)
updateFunc func(kube.Clients, string, runtime.Object) error
deleteFunc func(kube.Clients, string, string) error
}{
{"Deployment", createTestDeploymentWithAnnotations, callbacks.UpdateDeployment, deleteTestDeployment},
{"DaemonSet", createTestDaemonSetWithAnnotations, callbacks.UpdateDaemonSet, deleteTestDaemonSet},
{"StatefulSet", createTestStatefulSetWithAnnotations, callbacks.UpdateStatefulSet, deleteTestStatefulSet},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource, err := tt.createFunc(clients, fixtures.namespace, "1")
assert.NoError(t, err)
err = tt.updateFunc(clients, fixtures.namespace, resource)
assert.NoError(t, err)
accessor, err := meta.Accessor(resource)
assert.NoError(t, err)
err = tt.deleteFunc(clients, fixtures.namespace, accessor.GetName())
assert.NoError(t, err)
})
}
}
func TestPatchResources(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
createFunc func(kube.Clients, string, string) (runtime.Object, error)
patchFunc func(kube.Clients, string, runtime.Object, patchtypes.PatchType, []byte) error
deleteFunc func(kube.Clients, string, string) error
assertFunc func(err error)
}{
{"Deployment", createTestDeploymentWithAnnotations, callbacks.PatchDeployment, deleteTestDeployment, func(err error) {
assert.NoError(t, err)
patchedResource, err := callbacks.GetDeploymentItem(clients, "test-deployment", fixtures.namespace)
assert.NoError(t, err)
assert.Equal(t, "test", patchedResource.(*appsv1.Deployment).Annotations["test"])
}},
{"DaemonSet", createTestDaemonSetWithAnnotations, callbacks.PatchDaemonSet, deleteTestDaemonSet, func(err error) {
assert.NoError(t, err)
patchedResource, err := callbacks.GetDaemonSetItem(clients, "test-daemonset", fixtures.namespace)
assert.NoError(t, err)
assert.Equal(t, "test", patchedResource.(*appsv1.DaemonSet).Annotations["test"])
}},
{"StatefulSet", createTestStatefulSetWithAnnotations, callbacks.PatchStatefulSet, deleteTestStatefulSet, func(err error) {
assert.NoError(t, err)
patchedResource, err := callbacks.GetStatefulSetItem(clients, "test-statefulset", fixtures.namespace)
assert.NoError(t, err)
assert.Equal(t, "test", patchedResource.(*appsv1.StatefulSet).Annotations["test"])
}},
{"CronJob", createTestCronJobWithAnnotations, callbacks.PatchCronJob, deleteTestCronJob, func(err error) {
assert.EqualError(t, err, "not supported patching: CronJob")
}},
{"Job", createTestJobWithAnnotations, callbacks.PatchJob, deleteTestJob, func(err error) {
assert.EqualError(t, err, "not supported patching: Job")
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource, err := tt.createFunc(clients, fixtures.namespace, "1")
assert.NoError(t, err)
err = tt.patchFunc(clients, fixtures.namespace, resource, patchtypes.StrategicMergePatchType, []byte(`{"metadata":{"annotations":{"test":"test"}}}`))
tt.assertFunc(err)
accessor, err := meta.Accessor(resource)
assert.NoError(t, err)
err = tt.deleteFunc(clients, fixtures.namespace, accessor.GetName())
assert.NoError(t, err)
})
}
}
func TestCreateJobFromCronjob(t *testing.T) {
fixtures := newTestFixtures()
runtimeObj, err := createTestCronJobWithAnnotations(clients, fixtures.namespace, "1")
assert.NoError(t, err)
cronJob := runtimeObj.(*batchv1.CronJob)
err = callbacks.CreateJobFromCronjob(clients, fixtures.namespace, cronJob)
assert.NoError(t, err)
jobList, err := clients.KubernetesClient.BatchV1().Jobs(fixtures.namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
ownerFound := false
for _, job := range jobList.Items {
if isControllerOwner("CronJob", cronJob.Name, job.OwnerReferences) {
ownerFound = true
break
}
}
assert.Truef(t, ownerFound, "Missing CronJob owner reference")
err = deleteTestCronJob(clients, fixtures.namespace, cronJob.Name)
assert.NoError(t, err)
}
func TestReCreateJobFromJob(t *testing.T) {
fixtures := newTestFixtures()
job, err := createTestJobWithAnnotations(clients, fixtures.namespace, "1")
assert.NoError(t, err)
err = callbacks.ReCreateJobFromjob(clients, fixtures.namespace, job.(*batchv1.Job))
assert.NoError(t, err)
err = deleteTestJob(clients, fixtures.namespace, "test-job")
assert.NoError(t, err)
}
func TestGetVolumes(t *testing.T) {
fixtures := newTestFixtures()
tests := []struct {
name string
resource runtime.Object
getFunc func(runtime.Object) []v1.Volume
}{
{"Deployment", createResourceWithVolumes(&appsv1.Deployment{}, fixtures.defaultVolumes), callbacks.GetDeploymentVolumes},
{"CronJob", createResourceWithVolumes(&batchv1.CronJob{}, fixtures.defaultVolumes), callbacks.GetCronJobVolumes},
{"Job", createResourceWithVolumes(&batchv1.Job{}, fixtures.defaultVolumes), callbacks.GetJobVolumes},
{"DaemonSet", createResourceWithVolumes(&appsv1.DaemonSet{}, fixtures.defaultVolumes), callbacks.GetDaemonSetVolumes},
{"StatefulSet", createResourceWithVolumes(&appsv1.StatefulSet{}, fixtures.defaultVolumes), callbacks.GetStatefulSetVolumes},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, fixtures.defaultVolumes, tt.getFunc(tt.resource))
})
}
}
func TesGetPatchTemplateAnnotation(t *testing.T) {
templates := callbacks.GetPatchTemplates()
assert.NotEmpty(t, templates.AnnotationTemplate)
assert.Equal(t, 2, strings.Count(templates.AnnotationTemplate, "%s"))
}
func TestGetPatchTemplateEnvVar(t *testing.T) {
templates := callbacks.GetPatchTemplates()
assert.NotEmpty(t, templates.EnvVarTemplate)
assert.Equal(t, 3, strings.Count(templates.EnvVarTemplate, "%s"))
}
func TestGetPatchDeleteTemplateEnvVar(t *testing.T) {
templates := callbacks.GetPatchTemplates()
assert.NotEmpty(t, templates.DeleteEnvVarTemplate)
assert.Equal(t, 2, strings.Count(templates.DeleteEnvVarTemplate, "%d"))
}
// Helper functions
func isRestartStrategy(rollout *argorolloutv1alpha1.Rollout) bool {
return rollout.Spec.RestartAt == nil
}
func watchRollout(name, namespace string) chan interface{} {
timeOut := int64(1)
modifiedChan := make(chan interface{})
watcher, _ := clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).Watch(context.Background(), metav1.ListOptions{TimeoutSeconds: &timeOut})
go watchModified(watcher, name, modifiedChan)
return modifiedChan
}
func watchModified(watcher watch.Interface, name string, modifiedChan chan interface{}) {
for event := range watcher.ResultChan() {
item := event.Object.(*argorolloutv1alpha1.Rollout)
if item.Name == name {
switch event.Type {
case watch.Modified:
modifiedChan <- nil
}
return
}
}
}
func createTestDeployments(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
_, err := testutil.CreateDeployment(clients.KubernetesClient, fmt.Sprintf("test-deployment-%d", i), namespace, false)
if err != nil {
return err
}
}
return nil
}
func deleteTestDeployments(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
err := testutil.DeleteDeployment(clients.KubernetesClient, namespace, fmt.Sprintf("test-deployment-%d", i))
if err != nil {
return err
}
}
return nil
}
func createTestCronJobs(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
_, err := testutil.CreateCronJob(clients.KubernetesClient, fmt.Sprintf("test-cron-%d", i), namespace, false)
if err != nil {
return err
}
}
return nil
}
func deleteTestCronJobs(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
err := testutil.DeleteCronJob(clients.KubernetesClient, namespace, fmt.Sprintf("test-cron-%d", i))
if err != nil {
return err
}
}
return nil
}
func createTestJobs(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
_, err := testutil.CreateJob(clients.KubernetesClient, fmt.Sprintf("test-job-%d", i), namespace, false)
if err != nil {
return err
}
}
return nil
}
func deleteTestJobs(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
err := testutil.DeleteJob(clients.KubernetesClient, namespace, fmt.Sprintf("test-job-%d", i))
if err != nil {
return err
}
}
return nil
}
func createTestDaemonSets(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
_, err := testutil.CreateDaemonSet(clients.KubernetesClient, fmt.Sprintf("test-daemonset-%d", i), namespace, false)
if err != nil {
return err
}
}
return nil
}
func deleteTestDaemonSets(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
err := testutil.DeleteDaemonSet(clients.KubernetesClient, namespace, fmt.Sprintf("test-daemonset-%d", i))
if err != nil {
return err
}
}
return nil
}
func createTestStatefulSets(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
_, err := testutil.CreateStatefulSet(clients.KubernetesClient, fmt.Sprintf("test-statefulset-%d", i), namespace, false)
if err != nil {
return err
}
}
return nil
}
func deleteTestStatefulSets(clients kube.Clients, namespace string) error {
for i := 1; i <= 2; i++ {
err := testutil.DeleteStatefulSet(clients.KubernetesClient, namespace, fmt.Sprintf("test-statefulset-%d", i))
if err != nil {
return err
}
}
return nil
}
func createResourceWithPodAnnotations(obj runtime.Object, annotations map[string]string) runtime.Object {
switch v := obj.(type) {
case *appsv1.Deployment:
v.Spec.Template.Annotations = annotations
case *appsv1.DaemonSet:
v.Spec.Template.Annotations = annotations
case *appsv1.StatefulSet:
v.Spec.Template.Annotations = annotations
case *batchv1.CronJob:
v.Spec.JobTemplate.Spec.Template.Annotations = annotations
case *batchv1.Job:
v.Spec.Template.Annotations = annotations
case *argorolloutv1alpha1.Rollout:
v.Spec.Template.Annotations = annotations
}
return obj
}
func createResourceWithContainers(obj runtime.Object, containers []v1.Container) runtime.Object {
switch v := obj.(type) {
case *appsv1.Deployment:
v.Spec.Template.Spec.Containers = containers
case *appsv1.DaemonSet:
v.Spec.Template.Spec.Containers = containers
case *appsv1.StatefulSet:
v.Spec.Template.Spec.Containers = containers
case *batchv1.CronJob:
v.Spec.JobTemplate.Spec.Template.Spec.Containers = containers
case *batchv1.Job:
v.Spec.Template.Spec.Containers = containers
case *argorolloutv1alpha1.Rollout:
v.Spec.Template.Spec.Containers = containers
}
return obj
}
func createResourceWithInitContainers(obj runtime.Object, initContainers []v1.Container) runtime.Object {
switch v := obj.(type) {
case *appsv1.Deployment:
v.Spec.Template.Spec.InitContainers = initContainers
case *appsv1.DaemonSet:
v.Spec.Template.Spec.InitContainers = initContainers
case *appsv1.StatefulSet:
v.Spec.Template.Spec.InitContainers = initContainers
case *batchv1.CronJob:
v.Spec.JobTemplate.Spec.Template.Spec.InitContainers = initContainers
case *batchv1.Job:
v.Spec.Template.Spec.InitContainers = initContainers
case *argorolloutv1alpha1.Rollout:
v.Spec.Template.Spec.InitContainers = initContainers
}
return obj
}
func createResourceWithVolumes(obj runtime.Object, volumes []v1.Volume) runtime.Object {
switch v := obj.(type) {
case *appsv1.Deployment:
v.Spec.Template.Spec.Volumes = volumes
case *batchv1.CronJob:
v.Spec.JobTemplate.Spec.Template.Spec.Volumes = volumes
case *batchv1.Job:
v.Spec.Template.Spec.Volumes = volumes
case *appsv1.DaemonSet:
v.Spec.Template.Spec.Volumes = volumes
case *appsv1.StatefulSet:
v.Spec.Template.Spec.Volumes = volumes
}
return obj
}
func createTestDeploymentWithAnnotations(clients kube.Clients, namespace, version string) (runtime.Object, error) {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Namespace: namespace,
Annotations: map[string]string{"version": version},
},
}
return clients.KubernetesClient.AppsV1().Deployments(namespace).Create(context.TODO(), deployment, metav1.CreateOptions{})
}
func deleteTestDeployment(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.AppsV1().Deployments(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func createTestDaemonSetWithAnnotations(clients kube.Clients, namespace, version string) (runtime.Object, error) {
daemonSet := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-daemonset",
Namespace: namespace,
Annotations: map[string]string{"version": version},
},
}
return clients.KubernetesClient.AppsV1().DaemonSets(namespace).Create(context.TODO(), daemonSet, metav1.CreateOptions{})
}
func deleteTestDaemonSet(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.AppsV1().DaemonSets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func createTestStatefulSetWithAnnotations(clients kube.Clients, namespace, version string) (runtime.Object, error) {
statefulSet := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-statefulset",
Namespace: namespace,
Annotations: map[string]string{"version": version},
},
}
return clients.KubernetesClient.AppsV1().StatefulSets(namespace).Create(context.TODO(), statefulSet, metav1.CreateOptions{})
}
func deleteTestStatefulSet(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.AppsV1().StatefulSets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func createTestCronJobWithAnnotations(clients kube.Clients, namespace, version string) (runtime.Object, error) {
cronJob := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cronjob",
Namespace: namespace,
Annotations: map[string]string{"version": version},
},
}
return clients.KubernetesClient.BatchV1().CronJobs(namespace).Create(context.TODO(), cronJob, metav1.CreateOptions{})
}
func deleteTestCronJob(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.BatchV1().CronJobs(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func createTestJobWithAnnotations(clients kube.Clients, namespace, version string) (runtime.Object, error) {
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "test-job",
Namespace: namespace,
Annotations: map[string]string{"version": version},
},
}
return clients.KubernetesClient.BatchV1().Jobs(namespace).Create(context.TODO(), job, metav1.CreateOptions{})
}
func deleteTestJob(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.BatchV1().Jobs(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func isControllerOwner(kind, name string, ownerRefs []metav1.OwnerReference) bool {
for _, ownerRef := range ownerRefs {
if *ownerRef.Controller && ownerRef.Kind == kind && ownerRef.Name == name {
return true
}
}
return false
}
-265
View File
@@ -1,265 +0,0 @@
package cmd
import (
"context"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/leadership"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stakater/Reloader/internal/pkg/controller"
"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"
)
// cfg holds the configuration for this reloader instance.
// It is populated by flag parsing and used throughout the application.
var cfg *config.Config
// NewReloaderCommand starts the reloader controller
func NewReloaderCommand() *cobra.Command {
// Create config with defaults
cfg = config.NewDefault()
cmd := &cobra.Command{
Use: "reloader",
Short: "A watcher for your Kubernetes cluster",
PreRunE: validateFlags,
Run: startReloader,
}
// Bind flags to the new config package
config.BindFlags(cmd.PersistentFlags(), cfg)
return cmd
}
func validateFlags(*cobra.Command, []string) error {
// Apply post-parse flag processing (converts string flags to proper types)
if err := config.ApplyFlags(cfg); err != nil {
return fmt.Errorf("applying flags: %w", err)
}
// Validate the configuration
if err := cfg.Validate(); err != nil {
return fmt.Errorf("validating config: %w", err)
}
// Sync new config to old options package for backward compatibility
// This bridge allows existing code to keep working during migration
syncConfigToOptions(cfg)
// Validate that HA options are correct
if cfg.EnableHA {
if err := validateHAEnvs(); err != nil {
return err
}
}
return nil
}
// syncConfigToOptions bridges the new Config struct to the old options package.
// This allows existing code to continue working during the migration period.
// TODO: Remove this once all code is migrated to use Config directly.
func syncConfigToOptions(cfg *config.Config) {
options.AutoReloadAll = cfg.AutoReloadAll
options.ConfigmapUpdateOnChangeAnnotation = cfg.Annotations.ConfigmapReload
options.SecretUpdateOnChangeAnnotation = cfg.Annotations.SecretReload
options.ReloaderAutoAnnotation = cfg.Annotations.Auto
options.ConfigmapReloaderAutoAnnotation = cfg.Annotations.ConfigmapAuto
options.SecretReloaderAutoAnnotation = cfg.Annotations.SecretAuto
options.IgnoreResourceAnnotation = cfg.Annotations.Ignore
options.ConfigmapExcludeReloaderAnnotation = cfg.Annotations.ConfigmapExclude
options.SecretExcludeReloaderAnnotation = cfg.Annotations.SecretExclude
options.AutoSearchAnnotation = cfg.Annotations.Search
options.SearchMatchAnnotation = cfg.Annotations.Match
options.RolloutStrategyAnnotation = cfg.Annotations.RolloutStrategy
options.PauseDeploymentAnnotation = cfg.Annotations.PausePeriod
options.PauseDeploymentTimeAnnotation = cfg.Annotations.PausedAt
options.LogFormat = cfg.LogFormat
options.LogLevel = cfg.LogLevel
options.WebhookUrl = cfg.WebhookURL
options.ResourcesToIgnore = cfg.IgnoredResources
options.WorkloadTypesToIgnore = cfg.IgnoredWorkloads
options.NamespacesToIgnore = cfg.IgnoredNamespaces
options.NamespaceSelectors = cfg.NamespaceSelectorStrings
options.ResourceSelectors = cfg.ResourceSelectorStrings
options.EnableHA = cfg.EnableHA
options.SyncAfterRestart = cfg.SyncAfterRestart
options.EnablePProf = cfg.EnablePProf
options.PProfAddr = cfg.PProfAddr
// Convert ReloadStrategy to string for old options
options.ReloadStrategy = string(cfg.ReloadStrategy)
// Convert bool flags to string for old options (IsArgoRollouts, ReloadOnCreate, ReloadOnDelete)
if cfg.ArgoRolloutsEnabled {
options.IsArgoRollouts = "true"
} else {
options.IsArgoRollouts = "false"
}
if cfg.ReloadOnCreate {
options.ReloadOnCreate = "true"
} else {
options.ReloadOnCreate = "false"
}
if cfg.ReloadOnDelete {
options.ReloadOnDelete = "true"
} else {
options.ReloadOnDelete = "false"
}
}
func configureLogging(logFormat, logLevel string) error {
switch logFormat {
case "json":
logrus.SetFormatter(&logrus.JSONFormatter{})
default:
// just let the library use default on empty string.
if logFormat != "" {
return fmt.Errorf("unsupported logging formatter: %q", logFormat)
}
}
// set log level
level, err := logrus.ParseLevel(logLevel)
if err != nil {
return err
}
logrus.SetLevel(level)
return nil
}
func validateHAEnvs() error {
podName, podNamespace := getHAEnvs()
if podName == "" {
return fmt.Errorf("%s not set, cannot run in HA mode without %s set", constants.PodNameEnv, constants.PodNameEnv)
}
if podNamespace == "" {
return fmt.Errorf("%s not set, cannot run in HA mode without %s set", constants.PodNamespaceEnv, constants.PodNamespaceEnv)
}
return nil
}
func getHAEnvs() (string, string) {
podName := os.Getenv(constants.PodNameEnv)
podNamespace := os.Getenv(constants.PodNamespaceEnv)
return podName, podNamespace
}
func startReloader(cmd *cobra.Command, args []string) {
common.GetCommandLineOptions()
err := configureLogging(cfg.LogFormat, cfg.LogLevel)
if err != nil {
logrus.Warn(err)
}
logrus.Info("Starting Reloader")
isGlobal := false
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
if len(currentNamespace) == 0 {
currentNamespace = v1.NamespaceAll
isGlobal = true
logrus.Warnf("KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
}
// create the clientset
clientset, err := kube.GetKubernetesClient()
if err != nil {
logrus.Fatal(err)
}
// Use config's IgnoredResources (already validated and normalized to lowercase)
ignoredResourcesList := util.List(cfg.IgnoredResources)
ignoredNamespacesList := cfg.IgnoredNamespaces
namespaceLabelSelector := ""
if isGlobal {
namespaceLabelSelector, err = common.GetNamespaceLabelSelector(options.NamespaceSelectors)
if err != nil {
logrus.Fatal(err)
}
}
resourceLabelSelector, err := common.GetResourceLabelSelector(options.ResourceSelectors)
if err != nil {
logrus.Fatal(err)
}
if len(namespaceLabelSelector) > 0 {
logrus.Warnf("namespace-selector is set, will only detect changes in namespaces with these labels: %s.", namespaceLabelSelector)
}
if len(resourceLabelSelector) > 0 {
logrus.Warnf("resource-label-selector is set, will only detect changes on resources with these labels: %s.", resourceLabelSelector)
}
if cfg.WebhookURL != "" {
logrus.Warnf("webhook-url is set, will only send webhook, no resources will be reloaded")
}
collectors := metrics.SetupPrometheusEndpoint()
var controllers []*controller.Controller
for k := range kube.ResourceMap {
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
continue
}
c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, resourceLabelSelector, collectors)
if err != nil {
logrus.Fatalf("%s", err)
}
controllers = append(controllers, c)
// If HA is enabled we only run the controller when we're the leader
if cfg.EnableHA {
continue
}
// Now let's start the controller
stop := make(chan struct{})
defer close(stop)
logrus.Infof("Starting Controller to watch resource type: %s", k)
go c.Run(1, stop)
}
// Run leadership election
if cfg.EnableHA {
podName, podNamespace := getHAEnvs()
lock := leadership.GetNewLock(clientset.CoordinationV1(), constants.LockName, podName, podNamespace)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go leadership.RunLeaderElection(lock, ctx, cancel, podName, controllers)
}
common.PublishMetaInfoConfigmap(clientset)
if cfg.EnablePProf {
go startPProfServer()
}
leadership.SetupLivenessEndpoint()
logrus.Fatal(http.ListenAndServe(cfg.MetricsAddr, nil))
}
func startPProfServer() {
logrus.Infof("Starting pprof server on %s", cfg.PProfAddr)
if err := http.ListenAndServe(cfg.PProfAddr, nil); err != nil {
logrus.Errorf("Failed to start pprof server: %v", err)
}
}
+52 -117
View File
@@ -1,5 +1,4 @@
// Package config provides configuration management for Reloader.
// It replaces the old global variables pattern with an immutable Config struct.
package config
import (
@@ -12,11 +11,7 @@ import (
type ReloadStrategy string
const (
// ReloadStrategyEnvVars adds/updates environment variables to trigger restart.
// This is the default and recommended strategy for GitOps compatibility.
ReloadStrategyEnvVars ReloadStrategy = "env-vars"
// ReloadStrategyAnnotations adds/updates pod template annotations to trigger restart.
ReloadStrategyEnvVars ReloadStrategy = "env-vars"
ReloadStrategyAnnotations ReloadStrategy = "annotations"
)
@@ -24,143 +19,81 @@ const (
type ArgoRolloutStrategy string
const (
// ArgoRolloutStrategyRestart uses the restart mechanism for Argo Rollouts.
ArgoRolloutStrategyRestart ArgoRolloutStrategy = "restart"
// ArgoRolloutStrategyRollout uses the rollout mechanism for Argo Rollouts.
ArgoRolloutStrategyRollout ArgoRolloutStrategy = "rollout"
)
// Config holds all configuration for Reloader.
// This struct is immutable after creation - all fields should be set during initialization.
type Config struct {
// Annotations holds customizable annotation keys.
Annotations AnnotationConfig
// AutoReloadAll enables automatic reload for all resources without requiring annotations.
AutoReloadAll bool
// ReloadStrategy determines how workload restarts are triggered.
ReloadStrategy ReloadStrategy
// ArgoRolloutsEnabled enables support for Argo Rollouts workload type.
Annotations AnnotationConfig
AutoReloadAll bool
ReloadStrategy ReloadStrategy
ArgoRolloutsEnabled bool
// ArgoRolloutStrategy determines how Argo Rollouts are updated.
ArgoRolloutStrategy ArgoRolloutStrategy
ReloadOnCreate bool
ReloadOnDelete bool
SyncAfterRestart bool
EnableHA bool
WebhookURL string
// ReloadOnCreate enables watching for resource creation events.
ReloadOnCreate bool
// ReloadOnDelete enables watching for resource deletion events.
ReloadOnDelete bool
// SyncAfterRestart triggers a sync operation after a restart is performed.
SyncAfterRestart bool
// EnableHA enables high-availability mode with leader election.
EnableHA bool
// WebhookURL is an optional URL to send notifications to instead of triggering reload.
WebhookURL string
// Filtering configuration
IgnoredResources []string // ConfigMaps/Secrets to ignore (case-insensitive)
IgnoredWorkloads []string // Workload types to ignore
IgnoredNamespaces []string // Namespaces to ignore
NamespaceSelectors []labels.Selector
ResourceSelectors []labels.Selector
// Raw selector strings (for backward compatibility with old code)
IgnoredResources []string
IgnoredWorkloads []string
IgnoredNamespaces []string
NamespaceSelectors []labels.Selector
ResourceSelectors []labels.Selector
NamespaceSelectorStrings []string
ResourceSelectorStrings []string
// Logging configuration
LogFormat string // "json" or "" for default
LogLevel string // trace, debug, info, warning, error, fatal, panic
// Metrics configuration
MetricsAddr string // Address to serve metrics on (default :9090)
// Health probe configuration
HealthAddr string // Address to serve health probes on (default :8081)
// Profiling configuration
LogFormat string
LogLevel string
MetricsAddr string
HealthAddr string
EnablePProf bool
PProfAddr string
// Alerting configuration
Alerting AlertingConfig
// Leader election configuration
LeaderElection LeaderElectionConfig
// WatchedNamespace limits watching to a specific namespace (empty = all namespaces)
Alerting AlertingConfig
LeaderElection LeaderElectionConfig
WatchedNamespace string
// SyncPeriod is the period for re-syncing watched resources
SyncPeriod time.Duration
SyncPeriod time.Duration
}
// AnnotationConfig holds all customizable annotation keys.
// AnnotationConfig holds customizable annotation keys.
type AnnotationConfig struct {
// Prefix is the base prefix for all annotations (default: reloader.stakater.com)
Prefix string
// Auto annotations
Auto string // reloader.stakater.com/auto
ConfigmapAuto string // configmap.reloader.stakater.com/auto
SecretAuto string // secret.reloader.stakater.com/auto
// Reload annotations (explicit resource names)
ConfigmapReload string // configmap.reloader.stakater.com/reload
SecretReload string // secret.reloader.stakater.com/reload
// Exclude annotations
ConfigmapExclude string // configmaps.exclude.reloader.stakater.com/reload
SecretExclude string // secrets.exclude.reloader.stakater.com/reload
// Ignore annotation
Ignore string // reloader.stakater.com/ignore
// Search/Match annotations
Search string // reloader.stakater.com/search
Match string // reloader.stakater.com/match
// Rollout strategy annotation
RolloutStrategy string // reloader.stakater.com/rollout-strategy
// Pause annotations
PausePeriod string // deployment.reloader.stakater.com/pause-period
PausedAt string // deployment.reloader.stakater.com/paused-at
// Last reloaded from annotation (set by Reloader)
LastReloadedFrom string // reloader.stakater.com/last-reloaded-from
Prefix string
Auto string
ConfigmapAuto string
SecretAuto string
ConfigmapReload string
SecretReload string
ConfigmapExclude string
SecretExclude string
Ignore string
Search string
Match string
RolloutStrategy string
PausePeriod string
PausedAt string
LastReloadedFrom string
}
// AlertingConfig holds configuration for alerting integrations.
type AlertingConfig struct {
// Enabled enables alerting notifications on reload events.
Enabled bool
// WebhookURL is the webhook URL to send alerts to.
Enabled bool
WebhookURL string
// Sink determines the alert format: "slack", "teams", "gchat", or "raw" (default).
Sink string
// Proxy is an optional HTTP proxy for webhook requests.
Proxy string
// Additional is optional context prepended to alert messages.
Sink string
Proxy string
Additional string
}
// LeaderElectionConfig holds configuration for leader election.
type LeaderElectionConfig struct {
LockName string
Namespace string
Identity string
LockName string
Namespace string
Identity string
LeaseDuration time.Duration
RenewDeadline time.Duration
RetryPeriod time.Duration
ReleaseOnCancel bool
}
// NewDefault creates a Config with default values.
@@ -189,7 +122,11 @@ func NewDefault() *Config {
PProfAddr: ":6060",
Alerting: AlertingConfig{},
LeaderElection: LeaderElectionConfig{
LockName: "stakater-reloader-lock",
LockName: "reloader-leader-election",
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
ReleaseOnCancel: true,
},
WatchedNamespace: "",
SyncPeriod: 0,
@@ -247,7 +184,6 @@ func (c *Config) IsNamespaceIgnored(namespace string) bool {
return false
}
// equalFold is a simple case-insensitive string comparison.
func equalFold(s, t string) bool {
if len(s) != len(t) {
return false
@@ -255,7 +191,6 @@ func equalFold(s, t string) bool {
for i := 0; i < len(s); i++ {
c1, c2 := s[i], t[i]
if c1 != c2 {
// Convert to lowercase for comparison
if 'A' <= c1 && c1 <= 'Z' {
c1 += 'a' - 'A'
}
+13 -1
View File
@@ -46,9 +46,21 @@ func BindFlags(fs *pflag.FlagSet, cfg *Config) {
fs.BoolVar(&cfg.SyncAfterRestart, "sync-after-restart", cfg.SyncAfterRestart,
"Trigger sync operation after restart")
// High availability
// High availability / Leader election
fs.BoolVar(&cfg.EnableHA, "enable-ha", cfg.EnableHA,
"Enable high-availability mode with leader election")
fs.StringVar(&cfg.LeaderElection.LockName, "leader-election-id", cfg.LeaderElection.LockName,
"Name of the lease resource for leader election")
fs.StringVar(&cfg.LeaderElection.Namespace, "leader-election-namespace", cfg.LeaderElection.Namespace,
"Namespace for the leader election lease (defaults to pod namespace)")
fs.DurationVar(&cfg.LeaderElection.LeaseDuration, "leader-election-lease-duration", cfg.LeaderElection.LeaseDuration,
"Duration that non-leader candidates will wait before attempting to acquire leadership")
fs.DurationVar(&cfg.LeaderElection.RenewDeadline, "leader-election-renew-deadline", cfg.LeaderElection.RenewDeadline,
"Duration that the acting leader will retry refreshing leadership before giving up")
fs.DurationVar(&cfg.LeaderElection.RetryPeriod, "leader-election-retry-period", cfg.LeaderElection.RetryPeriod,
"Duration between leader election retries")
fs.BoolVar(&cfg.LeaderElection.ReleaseOnCancel, "leader-election-release-on-cancel", cfg.LeaderElection.ReleaseOnCancel,
"Release the leader lock when the manager is stopped")
// Webhook
fs.StringVar(&cfg.WebhookURL, "webhook-url", cfg.WebhookURL,
+1 -26
View File
@@ -1,32 +1,7 @@
package constants
// Environment variable names for pod identity in HA mode.
const (
// DefaultHttpListenAddr is the default listening address for global http server
DefaultHttpListenAddr = ":9090"
// ConfigmapEnvVarPostfix is a postfix for configmap envVar
ConfigmapEnvVarPostfix = "CONFIGMAP"
// SecretEnvVarPostfix is a postfix for secret envVar
SecretEnvVarPostfix = "SECRET"
// EnvVarPrefix is a Prefix for environment variable
EnvVarPrefix = "STAKATER_"
// ReloaderAnnotationPrefix is a Prefix for all reloader annotations
ReloaderAnnotationPrefix = "reloader.stakater.com"
// LastReloadedFromAnnotation is an annotation used to describe the last resource that triggered a reload
LastReloadedFromAnnotation = "last-reloaded-from"
// ReloadStrategyFlag The reload strategy flag name
ReloadStrategyFlag = "reload-strategy"
// EnvVarsReloadStrategy instructs Reloader to add container environment variables to facilitate a restart
EnvVarsReloadStrategy = "env-vars"
// AnnotationsReloadStrategy instructs Reloader to add pod template annotations to facilitate a restart
AnnotationsReloadStrategy = "annotations"
)
// Leadership election related consts
const (
LockName string = "stakater-reloader-lock"
PodNameEnv string = "POD_NAME"
PodNamespaceEnv string = "POD_NAMESPACE"
)
-15
View File
@@ -1,15 +0,0 @@
package constants
// Result is a status for deployment update
type Result int
const (
// Updated is returned when environment variable is created/updated
Updated Result = 1 + iota
// NotUpdated is returned when environment variable is found but had value equals to the new value
NotUpdated
// NoEnvVarFound is returned when no environment variable is found
NoEnvVarFound
// NoContainerFound is returned when no environment variable is found
NoContainerFound
)
@@ -6,6 +6,7 @@ import (
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/events"
"github.com/stakater/Reloader/internal/pkg/metrics"
@@ -33,6 +34,7 @@ type ConfigMapReconciler struct {
Collectors *metrics.Collectors
EventRecorder *events.Recorder
WebhookClient *webhook.Client
Alerter alerting.Alerter
initialized bool
initOnce sync.Once
@@ -131,6 +133,19 @@ func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "ConfigMap",
ResourceName: cm.Name,
ResourceNamespace: cm.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
@@ -202,6 +217,19 @@ func (r *ConfigMapReconciler) handleDelete(ctx context.Context, req ctrl.Request
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "ConfigMap", req.Name)
r.recordMetrics(true, req.Namespace)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "ConfigMap",
ResourceName: req.Name,
ResourceNamespace: req.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
-282
View File
@@ -1,282 +0,0 @@
package controller
import (
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/handler"
"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/kube"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/utils/strings/slices"
)
// Controller for checking events
type Controller struct {
client kubernetes.Interface
indexer cache.Indexer
queue workqueue.TypedRateLimitingInterface[any]
informer cache.Controller
namespace string
resource string
ignoredNamespaces util.List
collectors metrics.Collectors
recorder record.EventRecorder
namespaceSelector string
resourceSelector string
}
// controllerInitialized flag determines whether controlled is being initialized
var secretControllerInitialized bool = false
var configmapControllerInitialized bool = false
var selectedNamespacesCache []string
// NewController for initializing a Controller
func NewController(
client kubernetes.Interface, resource string, namespace string, ignoredNamespaces []string, namespaceLabelSelector string, resourceLabelSelector string, collectors metrics.Collectors) (*Controller, error) {
if options.SyncAfterRestart {
secretControllerInitialized = true
configmapControllerInitialized = true
}
c := Controller{
client: client,
namespace: namespace,
ignoredNamespaces: ignoredNamespaces,
namespaceSelector: namespaceLabelSelector,
resourceSelector: resourceLabelSelector,
resource: resource,
}
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{
Interface: client.CoreV1().Events(""),
})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: fmt.Sprintf("reloader-%s", resource)})
queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]())
optionsModifier := func(options *metav1.ListOptions) {
if resource == "namespaces" {
options.LabelSelector = c.namespaceSelector
} else if len(c.resourceSelector) > 0 {
options.LabelSelector = c.resourceSelector
} else {
options.FieldSelector = fields.Everything().String()
}
}
listWatcher := cache.NewFilteredListWatchFromClient(client.CoreV1().RESTClient(), resource, namespace, optionsModifier)
_, informer := cache.NewInformerWithOptions(cache.InformerOptions{
ListerWatcher: listWatcher,
ObjectType: kube.ResourceMap[resource],
ResyncPeriod: 0,
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: c.Add,
UpdateFunc: c.Update,
DeleteFunc: c.Delete,
},
Indexers: cache.Indexers{},
})
c.informer = informer
c.queue = queue
c.collectors = collectors
c.recorder = recorder
logrus.Infof("created controller for: %s", resource)
return &c, nil
}
// Add function to add a new object to the queue in case of creating a resource
func (c *Controller) Add(obj interface{}) {
switch object := obj.(type) {
case *v1.Namespace:
c.addSelectedNamespaceToCache(*object)
return
}
if options.ReloadOnCreate == "true" {
if !c.resourceInIgnoredNamespace(obj) && c.resourceInSelectedNamespaces(obj) && secretControllerInitialized && configmapControllerInitialized {
c.queue.Add(handler.ResourceCreatedHandler{
Resource: obj,
Collectors: c.collectors,
Recorder: c.recorder,
})
}
}
}
func (c *Controller) resourceInIgnoredNamespace(raw interface{}) bool {
switch object := raw.(type) {
case *v1.ConfigMap:
return c.ignoredNamespaces.Contains(object.Namespace)
case *v1.Secret:
return c.ignoredNamespaces.Contains(object.Namespace)
}
return false
}
func (c *Controller) resourceInSelectedNamespaces(raw interface{}) bool {
if len(c.namespaceSelector) == 0 {
return true
}
switch object := raw.(type) {
case *v1.ConfigMap:
if slices.Contains(selectedNamespacesCache, object.GetNamespace()) {
return true
}
case *v1.Secret:
if slices.Contains(selectedNamespacesCache, object.GetNamespace()) {
return true
}
}
return false
}
func (c *Controller) addSelectedNamespaceToCache(namespace v1.Namespace) {
selectedNamespacesCache = append(selectedNamespacesCache, namespace.GetName())
logrus.Infof("added namespace to be watched: %s", namespace.GetName())
}
func (c *Controller) removeSelectedNamespaceFromCache(namespace v1.Namespace) {
for i, v := range selectedNamespacesCache {
if v == namespace.GetName() {
selectedNamespacesCache = append(selectedNamespacesCache[:i], selectedNamespacesCache[i+1:]...)
logrus.Infof("removed namespace from watch: %s", namespace.GetName())
return
}
}
}
// Update function to add an old object and a new object to the queue in case of updating a resource
func (c *Controller) Update(old interface{}, new interface{}) {
switch new.(type) {
case *v1.Namespace:
return
}
if !c.resourceInIgnoredNamespace(new) && c.resourceInSelectedNamespaces(new) {
c.queue.Add(handler.ResourceUpdatedHandler{
Resource: new,
OldResource: old,
Collectors: c.collectors,
Recorder: c.recorder,
})
}
}
// Delete function to add an object to the queue in case of deleting a resource
func (c *Controller) Delete(old interface{}) {
if options.ReloadOnDelete == "true" {
if !c.resourceInIgnoredNamespace(old) && c.resourceInSelectedNamespaces(old) && secretControllerInitialized && configmapControllerInitialized {
c.queue.Add(handler.ResourceDeleteHandler{
Resource: old,
Collectors: c.collectors,
Recorder: c.recorder,
})
}
}
switch object := old.(type) {
case *v1.Namespace:
c.removeSelectedNamespaceFromCache(*object)
return
}
}
// Run function for controller which handles the queue
func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
defer runtime.HandleCrash()
// Let the workers stop when we are done
defer c.queue.ShutDown()
go c.informer.Run(stopCh)
// Wait for all involved caches to be synced, before processing items from the queue is started
if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
return
}
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
logrus.Infof("Stopping Controller")
}
func (c *Controller) runWorker() {
// At this point the controller is fully initialized and we can start processing the resources
if c.resource == string(v1.ResourceSecrets) {
secretControllerInitialized = true
} else if c.resource == string(v1.ResourceConfigMaps) {
configmapControllerInitialized = true
}
for c.processNextItem() {
}
}
func (c *Controller) processNextItem() bool {
// Wait until there is a new item in the working queue
resourceHandler, quit := c.queue.Get()
if quit {
return false
}
// Tell the queue that we are done with processing this key. This unblocks the key for other workers
// This allows safe parallel processing because two events with the same key are never processed in
// parallel.
defer c.queue.Done(resourceHandler)
// Invoke the method containing the business logic
err := resourceHandler.(handler.ResourceHandler).Handle()
// Handle the error if something went wrong during the execution of the business logic
c.handleErr(err, resourceHandler)
return true
}
// handleErr checks if an error happened and makes sure we will retry later.
func (c *Controller) handleErr(err error, key interface{}) {
if err == nil {
// Forget about the #AddRateLimited history of the key on every successful synchronization.
// This ensures that future processing of updates for this key is not delayed because of
// an outdated error history.
c.queue.Forget(key)
return
}
// This controller retries 5 times if something goes wrong. After that, it stops trying.
if c.queue.NumRequeues(key) < 5 {
logrus.Errorf("Error syncing events: %v", err)
// Re-enqueue the key rate limited. Based on the rate limiter on the
// queue and the re-enqueue history, the key will be processed later again.
c.queue.AddRateLimited(key)
return
}
c.queue.Forget(key)
// Report to an external entity that, even after several retries, we could not successfully process this key
runtime.HandleError(err)
logrus.Errorf("Dropping key out of the queue: %v", err)
logrus.Debugf("Dropping the key %q out of the queue: %v", key, err)
}
File diff suppressed because it is too large Load Diff
+55 -10
View File
@@ -3,10 +3,10 @@ package controller
import (
"context"
"fmt"
"time"
argorolloutsv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/events"
"github.com/stakater/Reloader/internal/pkg/metrics"
@@ -36,12 +36,10 @@ type ManagerOptions struct {
}
// NewManager creates a new controller-runtime manager with the given options.
// This follows controller-runtime and operator-sdk conventions for leader election.
func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
cfg := opts.Config
leaseDuration := 15 * time.Second
renewDeadline := 10 * time.Second
retryPeriod := 2 * time.Second
le := cfg.LeaderElection
mgrOpts := ctrl.Options{
Scheme: runtimeScheme,
@@ -49,11 +47,19 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
BindAddress: cfg.MetricsAddr,
},
HealthProbeBindAddress: cfg.HealthAddr,
LeaderElection: cfg.EnableHA,
LeaderElectionID: "reloader-leader-election",
LeaseDuration: &leaseDuration,
RenewDeadline: &renewDeadline,
RetryPeriod: &retryPeriod,
// Leader election configuration following operator-sdk best practices:
// - LeaderElection enables/disables leader election
// - LeaderElectionID is the name of the lease resource
// - LeaderElectionNamespace where the lease is created (defaults to pod namespace)
// - LeaderElectionReleaseOnCancel allows faster failover by releasing the lock on shutdown
LeaderElection: cfg.EnableHA,
LeaderElectionID: le.LockName,
LeaderElectionNamespace: le.Namespace,
LeaderElectionReleaseOnCancel: le.ReleaseOnCancel,
LeaseDuration: &le.LeaseDuration,
RenewDeadline: &le.RenewDeadline,
RetryPeriod: &le.RetryPeriod,
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts)
@@ -61,6 +67,10 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
return nil, fmt.Errorf("creating manager: %w", err)
}
// Add health and readiness probes.
// The healthz probe reports whether the manager is running.
// The readyz probe reports whether the manager is ready to serve requests.
// When leader election is enabled, readyz will fail until this instance becomes leader.
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return nil, fmt.Errorf("setting up health check: %w", err)
}
@@ -76,6 +86,13 @@ func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, col
registry := workload.NewRegistry(cfg.ArgoRolloutsEnabled)
reloadService := reload.NewService(cfg)
eventRecorder := events.NewRecorder(mgr.GetEventRecorderFor("reloader"))
pauseHandler := reload.NewPauseHandler(cfg)
// Create alerter based on configuration
alerter := alerting.NewAlerter(cfg)
if cfg.Alerting.Enabled {
log.Info("alerting enabled", "sink", cfg.Alerting.Sink)
}
// Create webhook client if URL is configured
var webhookClient *webhook.Client
@@ -84,6 +101,7 @@ func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, col
log.Info("webhook mode enabled", "url", cfg.WebhookURL)
}
// Setup ConfigMap reconciler
if !cfg.IsResourceIgnored("configmaps") {
if err := (&ConfigMapReconciler{
Client: mgr.GetClient(),
@@ -94,11 +112,13 @@ func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, col
Collectors: collectors,
EventRecorder: eventRecorder,
WebhookClient: webhookClient,
Alerter: alerter,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up configmap reconciler: %w", err)
}
}
// Setup Secret reconciler
if !cfg.IsResourceIgnored("secrets") {
if err := (&SecretReconciler{
Client: mgr.GetClient(),
@@ -109,11 +129,36 @@ func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, col
Collectors: collectors,
EventRecorder: eventRecorder,
WebhookClient: webhookClient,
Alerter: alerter,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up secret reconciler: %w", err)
}
}
// Setup Namespace reconciler if namespace selectors are configured
if len(cfg.NamespaceSelectors) > 0 {
nsCache := NewNamespaceCache(true)
if err := (&NamespaceReconciler{
Client: mgr.GetClient(),
Log: log.WithName("namespace-reconciler"),
Config: cfg,
Cache: nsCache,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up namespace reconciler: %w", err)
}
log.Info("namespace reconciler enabled for label selector filtering")
}
// Setup Deployment reconciler for pause handling
if err := (&DeploymentReconciler{
Client: mgr.GetClient(),
Log: log.WithName("deployment-reconciler"),
Config: cfg,
PauseHandler: pauseHandler,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up deployment reconciler: %w", err)
}
return nil
}
@@ -6,6 +6,7 @@ import (
"time"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/alerting"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/events"
"github.com/stakater/Reloader/internal/pkg/metrics"
@@ -33,6 +34,7 @@ type SecretReconciler struct {
Collectors *metrics.Collectors
EventRecorder *events.Recorder
WebhookClient *webhook.Client
Alerter alerting.Alerter
initialized bool
initOnce sync.Once
@@ -131,6 +133,19 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
"workload", decision.Workload.GetName(),
"kind", decision.Workload.Kind(),
)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "Secret",
ResourceName: secret.Name,
ResourceNamespace: secret.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
@@ -199,6 +214,19 @@ func (r *SecretReconciler) handleDelete(ctx context.Context, req ctrl.Request, l
if updated {
r.EventRecorder.ReloadSuccess(decision.Workload.GetObject(), "Secret", req.Name)
r.recordMetrics(true, req.Namespace)
// Send alert notification
if err := r.Alerter.Send(ctx, alerting.AlertMessage{
WorkloadKind: string(decision.Workload.Kind()),
WorkloadName: decision.Workload.GetName(),
WorkloadNamespace: decision.Workload.GetNamespace(),
ResourceKind: "Secret",
ResourceName: req.Name,
ResourceNamespace: req.Namespace,
Timestamp: time.Now(),
}); err != nil {
log.Error(err, "failed to send alert")
}
}
}
-20
View File
@@ -1,20 +0,0 @@
package crypto
import (
"crypto/sha1"
"fmt"
"io"
"github.com/sirupsen/logrus"
)
// GenerateSHA generates SHA from string
func GenerateSHA(data string) string {
hasher := sha1.New()
_, err := io.WriteString(hasher, data)
if err != nil {
logrus.Errorf("Unable to write data in hash writer %v", err)
}
sha := hasher.Sum(nil)
return fmt.Sprintf("%x", sha)
}
-15
View File
@@ -1,15 +0,0 @@
package crypto
import (
"testing"
)
// TestGenerateSHA generates the sha from given data and verifies whether it is correct or not
func TestGenerateSHA(t *testing.T) {
data := "www.stakater.com"
sha := "abd4ed82fb04548388a6cf3c339fd9dc84d275df"
result := GenerateSHA(data)
if result != sha {
t.Errorf("Failed to generate SHA")
}
}
-47
View File
@@ -1,47 +0,0 @@
package handler
import (
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
)
// ResourceCreatedHandler contains new objects
type ResourceCreatedHandler struct {
Resource interface{}
Collectors metrics.Collectors
Recorder record.EventRecorder
}
// Handle processes the newly created resource
func (r ResourceCreatedHandler) Handle() error {
if r.Resource == nil {
logrus.Errorf("Resource creation handler received nil resource")
} else {
config, _ := r.GetConfig()
// Send webhook
if options.WebhookUrl != "" {
return sendUpgradeWebhook(config, options.WebhookUrl)
}
// process resource based on its type
return doRollingUpgrade(config, r.Collectors, r.Recorder, invokeReloadStrategy)
}
return nil
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceCreatedHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}
return config, oldSHAData
}
-100
View File
@@ -1,100 +0,0 @@
package handler
import (
"fmt"
"slices"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
patchtypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
)
// ResourceDeleteHandler contains new objects
type ResourceDeleteHandler struct {
Resource interface{}
Collectors metrics.Collectors
Recorder record.EventRecorder
}
// Handle processes resources being deleted
func (r ResourceDeleteHandler) Handle() error {
if r.Resource == nil {
logrus.Errorf("Resource delete handler received nil resource")
} else {
config, _ := r.GetConfig()
// Send webhook
if options.WebhookUrl != "" {
return sendUpgradeWebhook(config, options.WebhookUrl)
}
// process resource based on its type
return doRollingUpgrade(config, r.Collectors, r.Recorder, invokeDeleteStrategy)
}
return nil
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceDeleteHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}
return config, oldSHAData
}
func invokeDeleteStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
if options.ReloadStrategy == constants.AnnotationsReloadStrategy {
return removePodAnnotations(upgradeFuncs, item, config, autoReload)
}
return removeContainerEnvVars(upgradeFuncs, item, config, autoReload)
}
func removePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
config.SHAValue = testutil.GetSHAfromEmptyData()
return updatePodAnnotations(upgradeFuncs, item, config, autoReload)
}
func removeContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
envVar := getEnvVarName(config.ResourceName, config.Type)
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)
if container == nil {
return InvokeStrategyResult{constants.NoContainerFound, nil}
}
//remove if env var exists
if len(container.Env) > 0 {
index := slices.IndexFunc(container.Env, func(envVariable v1.EnvVar) bool {
return envVariable.Name == envVar
})
if index != -1 {
var patch []byte
if upgradeFuncs.SupportsPatch {
containers := upgradeFuncs.ContainersFunc(item)
containerIndex := slices.IndexFunc(containers, func(c v1.Container) bool {
return c.Name == container.Name
})
patch = fmt.Appendf(nil, upgradeFuncs.PatchTemplatesFunc().DeleteEnvVarTemplate, containerIndex, index)
}
container.Env = append(container.Env[:index], container.Env[index+1:]...)
return InvokeStrategyResult{constants.Updated, &Patch{Type: patchtypes.JSONPatchType, Bytes: patch}}
}
}
return InvokeStrategyResult{constants.NotUpdated, nil}
}
-9
View File
@@ -1,9 +0,0 @@
package handler
import "github.com/stakater/Reloader/pkg/common"
// ResourceHandler handles the creation and update of resources
type ResourceHandler interface {
Handle() error
GetConfig() (common.Config, string)
}
-242
View File
@@ -1,242 +0,0 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
app "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
patchtypes "k8s.io/apimachinery/pkg/types"
)
// Keeps track of currently active timers
var activeTimers = make(map[string]*time.Timer)
// Returns unique key for the activeTimers map
func getTimerKey(namespace, deploymentName string) string {
return fmt.Sprintf("%s/%s", namespace, deploymentName)
}
// Checks if a deployment is currently paused
func IsPaused(deployment *app.Deployment) bool {
return deployment.Spec.Paused
}
// Deployment paused by reloader ?
func IsPausedByReloader(deployment *app.Deployment) bool {
if IsPaused(deployment) {
pausedAtAnnotationValue := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
return pausedAtAnnotationValue != ""
}
return false
}
// Returns the time, the deployment was paused by reloader, nil otherwise
func GetPauseStartTime(deployment *app.Deployment) (*time.Time, error) {
if !IsPausedByReloader(deployment) {
return nil, nil
}
pausedAtStr := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
parsedTime, err := time.Parse(time.RFC3339, pausedAtStr)
if err != nil {
return nil, err
}
return &parsedTime, nil
}
// ParsePauseDuration parses the pause interval value and returns a time.Duration
func ParsePauseDuration(pauseIntervalValue string) (time.Duration, error) {
pauseDuration, err := time.ParseDuration(pauseIntervalValue)
if err != nil {
logrus.Warnf("Failed to parse pause interval value '%s': %v", pauseIntervalValue, err)
return 0, err
}
return pauseDuration, nil
}
// Pauses a deployment for a specified duration and creates a timer to resume it
// after the specified duration
func PauseDeployment(deployment *app.Deployment, clients kube.Clients, namespace, pauseIntervalValue string) (*app.Deployment, error) {
deploymentName := deployment.Name
pauseDuration, err := ParsePauseDuration(pauseIntervalValue)
if err != nil {
return nil, err
}
if !IsPaused(deployment) {
logrus.Infof("Pausing Deployment '%s' in namespace '%s' for %s", deploymentName, namespace, pauseDuration)
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
pausePatch, err := CreatePausePatch()
if err != nil {
logrus.Errorf("Failed to create pause patch for deployment '%s': %v", deploymentName, err)
return deployment, err
}
err = deploymentFuncs.PatchFunc(clients, namespace, deployment, patchtypes.StrategicMergePatchType, pausePatch)
if err != nil {
logrus.Errorf("Failed to patch deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return deployment, err
}
updatedDeployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
CreateResumeTimer(deployment, clients, namespace, pauseDuration)
return updatedDeployment, err
}
if !IsPausedByReloader(deployment) {
logrus.Infof("Deployment '%s' in namespace '%s' already paused", deploymentName, namespace)
return deployment, nil
}
// Deployment has already been paused by reloader, check for timer
logrus.Debugf("Deployment '%s' in namespace '%s' is already paused by reloader", deploymentName, namespace)
timerKey := getTimerKey(namespace, deploymentName)
_, timerExists := activeTimers[timerKey]
if !timerExists {
logrus.Warnf("Timer does not exist for already paused deployment '%s' in namespace '%s', creating new one",
deploymentName, namespace)
HandleMissingTimer(deployment, pauseDuration, clients, namespace)
}
return deployment, nil
}
// Handles the case where missing timers for deployments that have been paused by reloader.
// Could occur after new leader election or reloader restart
func HandleMissingTimer(deployment *app.Deployment, pauseDuration time.Duration, clients kube.Clients, namespace string) {
deploymentName := deployment.Name
pauseStartTime, err := GetPauseStartTime(deployment)
if err != nil {
logrus.Errorf("Error parsing pause start time for deployment '%s' in namespace '%s': %v. Resuming deployment immediately",
deploymentName, namespace, err)
ResumeDeployment(deployment, namespace, clients)
return
}
if pauseStartTime == nil {
return
}
elapsedPauseTime := time.Since(*pauseStartTime)
remainingPauseTime := pauseDuration - elapsedPauseTime
if remainingPauseTime <= 0 {
logrus.Infof("Pause period for deployment '%s' in namespace '%s' has expired. Resuming immediately",
deploymentName, namespace)
ResumeDeployment(deployment, namespace, clients)
return
}
logrus.Infof("Creating missing timer for already paused deployment '%s' in namespace '%s' with remaining time %s",
deploymentName, namespace, remainingPauseTime)
CreateResumeTimer(deployment, clients, namespace, remainingPauseTime)
}
// CreateResumeTimer creates a timer to resume the deployment after the specified duration
func CreateResumeTimer(deployment *app.Deployment, clients kube.Clients, namespace string, pauseDuration time.Duration) {
deploymentName := deployment.Name
timerKey := getTimerKey(namespace, deployment.Name)
// Check if there's an existing timer for this deployment
if _, exists := activeTimers[timerKey]; exists {
logrus.Debugf("Timer already exists for deployment '%s' in namespace '%s', Skipping creation",
deploymentName, namespace)
return
}
// Create and store the new timer
timer := time.AfterFunc(pauseDuration, func() {
ResumeDeployment(deployment, namespace, clients)
})
// Add the new timer to the map
activeTimers[timerKey] = timer
logrus.Debugf("Created pause timer for deployment '%s' in namespace '%s' with duration %s",
deploymentName, namespace, pauseDuration)
}
// ResumeDeployment resumes a deployment that has been paused by reloader
func ResumeDeployment(deployment *app.Deployment, namespace string, clients kube.Clients) {
deploymentName := deployment.Name
currentDeployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return
}
if !IsPausedByReloader(currentDeployment) {
logrus.Infof("Deployment '%s' in namespace '%s' not paused by Reloader. Skipping resume", deploymentName, namespace)
return
}
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
resumePatch, err := CreateResumePatch()
if err != nil {
logrus.Errorf("Failed to create resume patch for deployment '%s': %v", deploymentName, err)
return
}
// Remove the timer
timerKey := getTimerKey(namespace, deploymentName)
if timer, exists := activeTimers[timerKey]; exists {
timer.Stop()
delete(activeTimers, timerKey)
logrus.Debugf("Removed pause timer for deployment '%s' in namespace '%s'", deploymentName, namespace)
}
err = deploymentFuncs.PatchFunc(clients, namespace, currentDeployment, patchtypes.StrategicMergePatchType, resumePatch)
if err != nil {
logrus.Errorf("Failed to resume deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return
}
logrus.Infof("Successfully resumed deployment '%s' in namespace '%s'", deploymentName, namespace)
}
func CreatePausePatch() ([]byte, error) {
patchData := map[string]interface{}{
"spec": map[string]interface{}{
"paused": true,
},
"metadata": map[string]interface{}{
"annotations": map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Format(time.RFC3339),
},
},
}
return json.Marshal(patchData)
}
func CreateResumePatch() ([]byte, error) {
patchData := map[string]interface{}{
"spec": map[string]interface{}{
"paused": false,
},
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
options.PauseDeploymentTimeAnnotation: nil,
},
},
}
return json.Marshal(patchData)
}
@@ -1,391 +0,0 @@
package handler
import (
"context"
"fmt"
"testing"
"time"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
testclient "k8s.io/client-go/kubernetes/fake"
)
func TestIsPaused(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
paused bool
}{
{
name: "paused deployment",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
paused: true,
},
{
name: "unpaused deployment",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
paused: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := IsPaused(test.deployment)
assert.Equal(t, test.paused, result)
})
}
}
func TestIsPausedByReloader(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
pausedByReloader bool
}{
{
name: "paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Format(time.RFC3339),
},
},
},
pausedByReloader: true,
},
{
name: "not paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
},
pausedByReloader: false,
},
{
name: "not paused",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
pausedByReloader: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pausedByReloader := IsPausedByReloader(test.deployment)
assert.Equal(t, test.pausedByReloader, pausedByReloader)
})
}
}
func TestGetPauseStartTime(t *testing.T) {
now := time.Now()
nowStr := now.Format(time.RFC3339)
tests := []struct {
name string
deployment *appsv1.Deployment
pausedByReloader bool
expectedStartTime time.Time
}{
{
name: "valid pause time",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: nowStr,
},
},
},
pausedByReloader: true,
expectedStartTime: now,
},
{
name: "not paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
pausedByReloader: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualStartTime, err := GetPauseStartTime(test.deployment)
assert.NoError(t, err)
if !test.pausedByReloader {
assert.Nil(t, actualStartTime)
} else {
assert.NotNil(t, actualStartTime)
assert.WithinDuration(t, test.expectedStartTime, *actualStartTime, time.Second)
}
})
}
}
func TestParsePauseDuration(t *testing.T) {
tests := []struct {
name string
pauseIntervalValue string
expectedDuration time.Duration
invalidDuration bool
}{
{
name: "valid duration",
pauseIntervalValue: "10s",
expectedDuration: 10 * time.Second,
invalidDuration: false,
},
{
name: "valid minute duration",
pauseIntervalValue: "2m",
expectedDuration: 2 * time.Minute,
invalidDuration: false,
},
{
name: "invalid duration",
pauseIntervalValue: "invalid",
expectedDuration: 0,
invalidDuration: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualDuration, err := ParsePauseDuration(test.pauseIntervalValue)
if test.invalidDuration {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expectedDuration, actualDuration)
}
})
}
}
func TestHandleMissingTimerSimple(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
shouldBePaused bool // Should be unpaused after HandleMissingTimer ?
}{
{
name: "deployment paused by reloader, pause period has expired and no timer",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-1",
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Add(-6 * time.Minute).Format(time.RFC3339),
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
shouldBePaused: false,
},
{
name: "deployment paused by reloader, pause period expires in the future and no timer",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-2",
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Add(1 * time.Minute).Format(time.RFC3339),
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
shouldBePaused: true,
},
}
for _, test := range tests {
// Clean up any timers at the end of the test
defer func() {
for key, timer := range activeTimers {
timer.Stop()
delete(activeTimers, key)
}
}()
t.Run(test.name, func(t *testing.T) {
fakeClient := testclient.NewSimpleClientset()
clients := kube.Clients{
KubernetesClient: fakeClient,
}
_, err := fakeClient.AppsV1().Deployments("default").Create(
context.TODO(),
test.deployment,
metav1.CreateOptions{})
assert.NoError(t, err, "Expected no error when creating deployment")
pauseDuration, _ := ParsePauseDuration(test.deployment.Annotations[options.PauseDeploymentAnnotation])
HandleMissingTimer(test.deployment, pauseDuration, clients, "default")
updatedDeployment, _ := fakeClient.AppsV1().Deployments("default").Get(context.TODO(), test.deployment.Name, metav1.GetOptions{})
assert.Equal(t, test.shouldBePaused, updatedDeployment.Spec.Paused,
"Deployment should have correct paused state after timer expiration")
if test.shouldBePaused {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.NotEmpty(t, pausedAtAnnotationValue,
"Pause annotation should be present and contain a value when deployment is paused")
}
})
}
}
func TestPauseDeployment(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
expectedError bool
expectedPaused bool
expectedAnnotation bool // Should have pause time annotation
pauseInterval string
}{
{
name: "deployment without pause annotation",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Annotations: map[string]string{},
},
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
expectedError: true,
expectedPaused: false,
expectedAnnotation: false,
pauseInterval: "",
},
{
name: "deployment already paused but not by reloader",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Annotations: map[string]string{
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
expectedError: false,
expectedPaused: true,
expectedAnnotation: false,
pauseInterval: "5m",
},
{
name: "deployment unpaused that needs to be paused by reloader",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-3",
Annotations: map[string]string{
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
expectedError: false,
expectedPaused: true,
expectedAnnotation: true,
pauseInterval: "5m",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := testclient.NewSimpleClientset()
clients := kube.Clients{
KubernetesClient: fakeClient,
}
_, err := fakeClient.AppsV1().Deployments("default").Create(
context.TODO(),
test.deployment,
metav1.CreateOptions{})
assert.NoError(t, err, "Expected no error when creating deployment")
updatedDeployment, err := PauseDeployment(test.deployment, clients, "default", test.pauseInterval)
if test.expectedError {
assert.Error(t, err, "Expected an error pausing the deployment")
return
} else {
assert.NoError(t, err, "Expected no error pausing the deployment")
}
assert.Equal(t, test.expectedPaused, updatedDeployment.Spec.Paused,
"Deployment should have correct paused state after pause")
if test.expectedAnnotation {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.NotEmpty(t, pausedAtAnnotationValue,
"Pause annotation should be present and contain a value when deployment is paused")
} else {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.Empty(t, pausedAtAnnotationValue,
"Pause annotation should not be present when deployment has not been paused by reloader")
}
})
}
}
// Simple helper function for test cases
func FindDeploymentByName(deployments []runtime.Object, deploymentName string) (*appsv1.Deployment, error) {
for _, deployment := range deployments {
accessor, err := meta.Accessor(deployment)
if err != nil {
return nil, fmt.Errorf("error getting accessor for item: %v", err)
}
if accessor.GetName() == deploymentName {
deploymentObj, ok := deployment.(*appsv1.Deployment)
if !ok {
return nil, fmt.Errorf("failed to cast to Deployment")
}
return deploymentObj, nil
}
}
return nil, fmt.Errorf("deployment '%s' not found", deploymentName)
}
-53
View File
@@ -1,53 +0,0 @@
package handler
import (
"github.com/sirupsen/logrus"
"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"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
)
// ResourceUpdatedHandler contains updated objects
type ResourceUpdatedHandler struct {
Resource interface{}
OldResource interface{}
Collectors metrics.Collectors
Recorder record.EventRecorder
}
// Handle processes the updated resource
func (r ResourceUpdatedHandler) Handle() error {
if r.Resource == nil || r.OldResource == nil {
logrus.Errorf("Resource update handler received nil resource")
} else {
config, oldSHAData := r.GetConfig()
if config.SHAValue != oldSHAData {
// Send a webhook if update
if options.WebhookUrl != "" {
return sendUpgradeWebhook(config, options.WebhookUrl)
}
// process resource based on its type
return doRollingUpgrade(config, r.Collectors, r.Recorder, invokeReloadStrategy)
}
}
return nil
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceUpdatedHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
oldSHAData = util.GetSHAfromConfigmap(r.OldResource.(*v1.ConfigMap))
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
oldSHAData = util.GetSHAfromSecret(r.OldResource.(*v1.Secret).Data)
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}
return config, oldSHAData
}
-619
View File
@@ -1,619 +0,0 @@
package handler
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"github.com/parnurzeal/gorequest"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
alert "github.com/stakater/Reloader/internal/pkg/alerts"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/constants"
"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"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
patchtypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
)
// GetDeploymentRollingUpgradeFuncs returns all callback funcs for a deployment
func GetDeploymentRollingUpgradeFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetDeploymentItem,
ItemsFunc: callbacks.GetDeploymentItems,
AnnotationsFunc: callbacks.GetDeploymentAnnotations,
PodAnnotationsFunc: callbacks.GetDeploymentPodAnnotations,
ContainersFunc: callbacks.GetDeploymentContainers,
InitContainersFunc: callbacks.GetDeploymentInitContainers,
UpdateFunc: callbacks.UpdateDeployment,
PatchFunc: callbacks.PatchDeployment,
PatchTemplatesFunc: callbacks.GetPatchTemplates,
VolumesFunc: callbacks.GetDeploymentVolumes,
ResourceType: "Deployment",
SupportsPatch: true,
}
}
// GetDeploymentRollingUpgradeFuncs returns all callback funcs for a cronjob
func GetCronJobCreateJobFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetCronJobItem,
ItemsFunc: callbacks.GetCronJobItems,
AnnotationsFunc: callbacks.GetCronJobAnnotations,
PodAnnotationsFunc: callbacks.GetCronJobPodAnnotations,
ContainersFunc: callbacks.GetCronJobContainers,
InitContainersFunc: callbacks.GetCronJobInitContainers,
UpdateFunc: callbacks.CreateJobFromCronjob,
PatchFunc: callbacks.PatchCronJob,
PatchTemplatesFunc: func() callbacks.PatchTemplates { return callbacks.PatchTemplates{} },
VolumesFunc: callbacks.GetCronJobVolumes,
ResourceType: "CronJob",
SupportsPatch: false,
}
}
// GetDeploymentRollingUpgradeFuncs returns all callback funcs for a cronjob
func GetJobCreateJobFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetJobItem,
ItemsFunc: callbacks.GetJobItems,
AnnotationsFunc: callbacks.GetJobAnnotations,
PodAnnotationsFunc: callbacks.GetJobPodAnnotations,
ContainersFunc: callbacks.GetJobContainers,
InitContainersFunc: callbacks.GetJobInitContainers,
UpdateFunc: callbacks.ReCreateJobFromjob,
PatchFunc: callbacks.PatchJob,
PatchTemplatesFunc: func() callbacks.PatchTemplates { return callbacks.PatchTemplates{} },
VolumesFunc: callbacks.GetJobVolumes,
ResourceType: "Job",
SupportsPatch: false,
}
}
// GetDaemonSetRollingUpgradeFuncs returns all callback funcs for a daemonset
func GetDaemonSetRollingUpgradeFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetDaemonSetItem,
ItemsFunc: callbacks.GetDaemonSetItems,
AnnotationsFunc: callbacks.GetDaemonSetAnnotations,
PodAnnotationsFunc: callbacks.GetDaemonSetPodAnnotations,
ContainersFunc: callbacks.GetDaemonSetContainers,
InitContainersFunc: callbacks.GetDaemonSetInitContainers,
UpdateFunc: callbacks.UpdateDaemonSet,
PatchFunc: callbacks.PatchDaemonSet,
PatchTemplatesFunc: callbacks.GetPatchTemplates,
VolumesFunc: callbacks.GetDaemonSetVolumes,
ResourceType: "DaemonSet",
SupportsPatch: true,
}
}
// GetStatefulSetRollingUpgradeFuncs returns all callback funcs for a statefulSet
func GetStatefulSetRollingUpgradeFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetStatefulSetItem,
ItemsFunc: callbacks.GetStatefulSetItems,
AnnotationsFunc: callbacks.GetStatefulSetAnnotations,
PodAnnotationsFunc: callbacks.GetStatefulSetPodAnnotations,
ContainersFunc: callbacks.GetStatefulSetContainers,
InitContainersFunc: callbacks.GetStatefulSetInitContainers,
UpdateFunc: callbacks.UpdateStatefulSet,
PatchFunc: callbacks.PatchStatefulSet,
PatchTemplatesFunc: callbacks.GetPatchTemplates,
VolumesFunc: callbacks.GetStatefulSetVolumes,
ResourceType: "StatefulSet",
SupportsPatch: true,
}
}
// GetArgoRolloutRollingUpgradeFuncs returns all callback funcs for a rollout
func GetArgoRolloutRollingUpgradeFuncs() callbacks.RollingUpgradeFuncs {
return callbacks.RollingUpgradeFuncs{
ItemFunc: callbacks.GetRolloutItem,
ItemsFunc: callbacks.GetRolloutItems,
AnnotationsFunc: callbacks.GetRolloutAnnotations,
PodAnnotationsFunc: callbacks.GetRolloutPodAnnotations,
ContainersFunc: callbacks.GetRolloutContainers,
InitContainersFunc: callbacks.GetRolloutInitContainers,
UpdateFunc: callbacks.UpdateRollout,
PatchFunc: callbacks.PatchRollout,
PatchTemplatesFunc: func() callbacks.PatchTemplates { return callbacks.PatchTemplates{} },
VolumesFunc: callbacks.GetRolloutVolumes,
ResourceType: "Rollout",
SupportsPatch: false,
}
}
func sendUpgradeWebhook(config common.Config, webhookUrl string) error {
logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s', Sending webhook to '%s'",
config.ResourceName, config.Type, config.Namespace, webhookUrl)
body, errs := sendWebhook(webhookUrl)
if errs != nil {
// return the first error
return errs[0]
} else {
logrus.Info(body)
}
return nil
}
func sendWebhook(url string) (string, []error) {
request := gorequest.New()
resp, _, err := request.Post(url).Send(`{"webhook":"update successful"}`).End()
if err != nil {
// the reloader seems to retry automatically so no retry logic added
return "", err
}
defer func() {
closeErr := resp.Body.Close()
if closeErr != nil {
logrus.Error(closeErr)
}
}()
var buffer bytes.Buffer
_, bufferErr := io.Copy(&buffer, resp.Body)
if bufferErr != nil {
logrus.Error(bufferErr)
}
return buffer.String(), nil
}
func doRollingUpgrade(config common.Config, collectors metrics.Collectors, recorder record.EventRecorder, invoke invokeStrategy) error {
clients := kube.GetClients()
// Get ignored workload types to avoid listing resources without RBAC permissions
ignoredWorkloadTypes, err := util.GetIgnoredWorkloadTypesList()
if err != nil {
logrus.Errorf("Failed to parse ignored workload types: %v", err)
ignoredWorkloadTypes = util.List{} // Continue with empty list if parsing fails
}
err = rollingUpgrade(clients, config, GetDeploymentRollingUpgradeFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
// Only process CronJobs if they are not ignored
if !ignoredWorkloadTypes.Contains("cronjobs") {
err = rollingUpgrade(clients, config, GetCronJobCreateJobFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
}
// Only process Jobs if they are not ignored
if !ignoredWorkloadTypes.Contains("jobs") {
err = rollingUpgrade(clients, config, GetJobCreateJobFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
}
err = rollingUpgrade(clients, config, GetDaemonSetRollingUpgradeFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
err = rollingUpgrade(clients, config, GetStatefulSetRollingUpgradeFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
if options.IsArgoRollouts == "true" {
err = rollingUpgrade(clients, config, GetArgoRolloutRollingUpgradeFuncs(), collectors, recorder, invoke)
if err != nil {
return err
}
}
return nil
}
func rollingUpgrade(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
err := PerformAction(clients, config, upgradeFuncs, collectors, recorder, strategy)
if err != nil {
logrus.Errorf("Rolling upgrade for '%s' failed with error = %v", config.ResourceName, err)
}
return err
}
// PerformAction invokes the deployment if there is any change in configmap or secret data
func PerformAction(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
items := upgradeFuncs.ItemsFunc(clients, config.Namespace)
for _, item := range items {
err := retryOnConflict(retry.DefaultRetry, func(fetchResource bool) error {
return upgradeResource(clients, config, upgradeFuncs, collectors, recorder, strategy, item, fetchResource)
})
if err != nil {
return err
}
}
return nil
}
func retryOnConflict(backoff wait.Backoff, fn func(_ bool) error) error {
var lastError error
fetchResource := false // do not fetch resource on first attempt, already done by ItemsFunc
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
err := fn(fetchResource)
fetchResource = true
switch {
case err == nil:
return true, nil
case apierrors.IsConflict(err):
lastError = err
return false, nil
default:
return false, err
}
})
if wait.Interrupted(err) {
err = lastError
}
return err
}
func upgradeResource(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy, resource runtime.Object, fetchResource bool) error {
accessor, err := meta.Accessor(resource)
if err != nil {
return err
}
resourceName := accessor.GetName()
if fetchResource {
resource, err = upgradeFuncs.ItemFunc(clients, resourceName, config.Namespace)
if err != nil {
return err
}
}
annotations := upgradeFuncs.AnnotationsFunc(resource)
podAnnotations := upgradeFuncs.PodAnnotationsFunc(resource)
result := common.ShouldReload(config, upgradeFuncs.ResourceType, annotations, podAnnotations, common.GetCommandLineOptions())
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
pauseInterval, foundPauseInterval := annotations[options.PauseDeploymentAnnotation]
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
}
}
}
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)
}
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)
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)
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 getVolumeMountName(volumes []v1.Volume, mountType string, volumeName string) string {
for i := range volumes {
switch mountType {
case constants.ConfigmapEnvVarPostfix:
if volumes[i].ConfigMap != nil && volumes[i].ConfigMap.Name == volumeName {
return volumes[i].Name
}
if volumes[i].Projected != nil {
for j := range volumes[i].Projected.Sources {
if volumes[i].Projected.Sources[j].ConfigMap != nil && volumes[i].Projected.Sources[j].ConfigMap.Name == volumeName {
return volumes[i].Name
}
}
}
case constants.SecretEnvVarPostfix:
if volumes[i].Secret != nil && volumes[i].Secret.SecretName == volumeName {
return volumes[i].Name
}
if volumes[i].Projected != nil {
for j := range volumes[i].Projected.Sources {
if volumes[i].Projected.Sources[j].Secret != nil && volumes[i].Projected.Sources[j].Secret.Name == volumeName {
return volumes[i].Name
}
}
}
}
}
return ""
}
func getContainerWithVolumeMount(containers []v1.Container, volumeMountName string) *v1.Container {
for i := range containers {
volumeMounts := containers[i].VolumeMounts
for j := range volumeMounts {
if volumeMounts[j].Name == volumeMountName {
return &containers[i]
}
}
}
return nil
}
func getContainerWithEnvReference(containers []v1.Container, resourceName string, resourceType string) *v1.Container {
for i := range containers {
envs := containers[i].Env
for j := range envs {
envVarSource := envs[j].ValueFrom
if envVarSource != nil {
if resourceType == constants.SecretEnvVarPostfix && envVarSource.SecretKeyRef != nil && envVarSource.SecretKeyRef.Name == resourceName {
return &containers[i]
} else if resourceType == constants.ConfigmapEnvVarPostfix && envVarSource.ConfigMapKeyRef != nil && envVarSource.ConfigMapKeyRef.Name == resourceName {
return &containers[i]
}
}
}
envsFrom := containers[i].EnvFrom
for j := range envsFrom {
if resourceType == constants.SecretEnvVarPostfix && envsFrom[j].SecretRef != nil && envsFrom[j].SecretRef.Name == resourceName {
return &containers[i]
} else if resourceType == constants.ConfigmapEnvVarPostfix && envsFrom[j].ConfigMapRef != nil && envsFrom[j].ConfigMapRef.Name == resourceName {
return &containers[i]
}
}
}
return nil
}
func getContainerUsingResource(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) *v1.Container {
volumes := upgradeFuncs.VolumesFunc(item)
containers := upgradeFuncs.ContainersFunc(item)
initContainers := upgradeFuncs.InitContainersFunc(item)
var container *v1.Container
// Get the volumeMountName to find volumeMount in container
volumeMountName := getVolumeMountName(volumes, config.Type, config.ResourceName)
// Get the container with mounted configmap/secret
if volumeMountName != "" {
container = getContainerWithVolumeMount(containers, volumeMountName)
if container == nil && len(initContainers) > 0 {
container = getContainerWithVolumeMount(initContainers, volumeMountName)
if container != nil {
// if configmap/secret is being used in init container then return the first Pod container to save reloader env
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
} else if container != nil {
return container
}
}
// Get the container with referenced secret or configmap as env var
container = getContainerWithEnvReference(containers, config.ResourceName, config.Type)
if container == nil && len(initContainers) > 0 {
container = getContainerWithEnvReference(initContainers, config.ResourceName, config.Type)
if container != nil {
// if configmap/secret is being used in init container then return the first Pod container to save reloader env
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
}
// Get the first container if the annotation is related to specified configmap or secret i.e. configmap.reloader.stakater.com/reload
if container == nil && !autoReload {
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
return container
}
type Patch struct {
Type patchtypes.PatchType
Bytes []byte
}
type InvokeStrategyResult struct {
Result constants.Result
Patch *Patch
}
type invokeStrategy func(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult
func invokeReloadStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
if options.ReloadStrategy == constants.AnnotationsReloadStrategy {
return updatePodAnnotations(upgradeFuncs, item, config, autoReload)
}
return updateContainerEnvVars(upgradeFuncs, item, config, autoReload)
}
func updatePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)
if container == nil {
return InvokeStrategyResult{constants.NoContainerFound, nil}
}
// Generate reloaded annotations. Attaching this to the item's annotation will trigger a rollout
// Note: the data on this struct is purely informational and is not used for future updates
reloadSource := common.NewReloadSourceFromConfig(config, []string{container.Name})
annotations, patch, err := createReloadedAnnotations(&reloadSource, upgradeFuncs)
if err != nil {
logrus.Errorf("Failed to create reloaded annotations for %s! error = %v", config.ResourceName, err)
return InvokeStrategyResult{constants.NotUpdated, nil}
}
// Copy the all annotations to the item's annotations
pa := upgradeFuncs.PodAnnotationsFunc(item)
if pa == nil {
return InvokeStrategyResult{constants.NotUpdated, nil}
}
for k, v := range annotations {
pa[k] = v
}
return InvokeStrategyResult{constants.Updated, &Patch{Type: patchtypes.StrategicMergePatchType, Bytes: patch}}
}
func getReloaderAnnotationKey() string {
return fmt.Sprintf("%s/%s",
constants.ReloaderAnnotationPrefix,
constants.LastReloadedFromAnnotation,
)
}
func createReloadedAnnotations(target *common.ReloadSource, upgradeFuncs callbacks.RollingUpgradeFuncs) (map[string]string, []byte, error) {
if target == nil {
return nil, nil, errors.New("target is required")
}
// Create a single "last-invokeReloadStrategy-from" annotation that stores metadata about the
// resource that caused the last invokeReloadStrategy.
// Intentionally only storing the last item in order to keep
// the generated annotations as small as possible.
annotations := make(map[string]string)
lastReloadedResourceName := getReloaderAnnotationKey()
lastReloadedResource, err := json.Marshal(target)
if err != nil {
return nil, nil, err
}
annotations[lastReloadedResourceName] = string(lastReloadedResource)
var patch []byte
if upgradeFuncs.SupportsPatch {
escapedValue, err := jsonEscape(annotations[lastReloadedResourceName])
if err != nil {
return nil, nil, err
}
patch = fmt.Appendf(nil, upgradeFuncs.PatchTemplatesFunc().AnnotationTemplate, lastReloadedResourceName, escapedValue)
}
return annotations, patch, nil
}
func getEnvVarName(resourceName string, typeName string) string {
return constants.EnvVarPrefix + util.ConvertToEnvVarName(resourceName) + "_" + typeName
}
func updateContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
envVar := getEnvVarName(config.ResourceName, config.Type)
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)
if container == nil {
return InvokeStrategyResult{constants.NoContainerFound, nil}
}
//update if env var exists
updateResult := updateEnvVar(container, envVar, config.SHAValue)
// if no existing env var exists lets create one
if updateResult == constants.NoEnvVarFound {
e := v1.EnvVar{
Name: envVar,
Value: config.SHAValue,
}
container.Env = append(container.Env, e)
updateResult = constants.Updated
}
var patch []byte
if upgradeFuncs.SupportsPatch {
patch = fmt.Appendf(nil, upgradeFuncs.PatchTemplatesFunc().EnvVarTemplate, container.Name, envVar, config.SHAValue)
}
return InvokeStrategyResult{updateResult, &Patch{Type: patchtypes.StrategicMergePatchType, Bytes: patch}}
}
func updateEnvVar(container *v1.Container, envVar string, shaData string) constants.Result {
envs := container.Env
for j := range envs {
if envs[j].Name == envVar {
if envs[j].Value != shaData {
envs[j].Value = shaData
return constants.Updated
}
return constants.NotUpdated
}
}
return constants.NoEnvVarFound
}
func jsonEscape(toEscape string) (string, error) {
bytes, err := json.Marshal(toEscape)
if err != nil {
return "", err
}
escaped := string(bytes)
return escaped[1 : len(escaped)-1], nil
}
File diff suppressed because it is too large Load Diff
-107
View File
@@ -1,107 +0,0 @@
package leadership
import (
"context"
"net/http"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/controller"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
)
var (
// Used for liveness probe
m sync.Mutex
healthy bool = true
)
func GetNewLock(client coordinationv1.CoordinationV1Interface, lockName, podname, namespace string) *resourcelock.LeaseLock {
return &resourcelock.LeaseLock{
LeaseMeta: v1.ObjectMeta{
Name: lockName,
Namespace: namespace,
},
Client: client,
LockConfig: resourcelock.ResourceLockConfig{
Identity: podname,
},
}
}
// runLeaderElection runs leadership election. If an instance of the controller is the leader and stops leading it will shutdown.
func RunLeaderElection(lock *resourcelock.LeaseLock, ctx context.Context, cancel context.CancelFunc, id string, controllers []*controller.Controller) {
// Construct channels for the controllers to use
var stopChannels []chan struct{}
for i := 0; i < len(controllers); i++ {
stop := make(chan struct{})
stopChannels = append(stopChannels, stop)
}
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
ReleaseOnCancel: true,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(c context.Context) {
logrus.Info("became leader, starting controllers")
runControllers(controllers, stopChannels)
},
OnStoppedLeading: func() {
logrus.Info("no longer leader, shutting down")
stopControllers(stopChannels)
cancel()
m.Lock()
defer m.Unlock()
healthy = false
},
OnNewLeader: func(current_id string) {
if current_id == id {
logrus.Info("still the leader!")
return
}
logrus.Infof("new leader is %s", current_id)
},
},
})
}
func runControllers(controllers []*controller.Controller, stopChannels []chan struct{}) {
for i, c := range controllers {
c := c
go c.Run(1, stopChannels[i])
}
}
func stopControllers(stopChannels []chan struct{}) {
for _, c := range stopChannels {
close(c)
}
}
// Healthz sets up the liveness probe endpoint. If leadership election is
// enabled and a replica stops leading the liveness probe will fail and the
// kubelet will restart the container.
func SetupLivenessEndpoint() {
http.HandleFunc("/live", healthz)
}
func healthz(w http.ResponseWriter, req *http.Request) {
m.Lock()
defer m.Unlock()
if healthy {
if i, err := w.Write([]byte("alive")); err != nil {
logrus.Infof("failed to write liveness response, wrote: %d bytes, got err: %s", i, err)
}
return
}
w.WriteHeader(http.StatusInternalServerError)
}
-216
View File
@@ -1,216 +0,0 @@
//go:build integration
// +build integration
package leadership
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/controller"
"github.com/stakater/Reloader/internal/pkg/handler"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
)
func TestMain(m *testing.M) {
testutil.CreateNamespace(testutil.Namespace, testutil.Clients.KubernetesClient)
logrus.Infof("Running Testcases")
retCode := m.Run()
testutil.DeleteNamespace(testutil.Namespace, testutil.Clients.KubernetesClient)
os.Exit(retCode)
}
func TestHealthz(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "/live", nil)
if err != nil {
t.Fatalf(("failed to create request"))
}
response := httptest.NewRecorder()
healthz(response, request)
got := response.Code
want := 200
if got != want {
t.Fatalf("got: %q, want: %q", got, want)
}
// Have the liveness probe serve a 500
healthy = false
request, err = http.NewRequest(http.MethodGet, "/live", nil)
if err != nil {
t.Fatalf(("failed to create request"))
}
response = httptest.NewRecorder()
healthz(response, request)
got = response.Code
want = 500
if got != want {
t.Fatalf("got: %q, want: %q", got, want)
}
}
// TestRunLeaderElection validates that the liveness endpoint serves 500 when
// leadership election fails
func TestRunLeaderElection(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
lock := GetNewLock(testutil.Clients.KubernetesClient.CoordinationV1(), constants.LockName, testutil.Pod, testutil.Namespace)
go RunLeaderElection(lock, ctx, cancel, testutil.Pod, []*controller.Controller{})
// Liveness probe should be serving OK
request, err := http.NewRequest(http.MethodGet, "/live", nil)
if err != nil {
t.Fatalf(("failed to create request"))
}
response := httptest.NewRecorder()
healthz(response, request)
got := response.Code
want := 500
if got != want {
t.Fatalf("got: %q, want: %q", got, want)
}
// Cancel the leader election context, so leadership is released and
// live endpoint serves 500
cancel()
request, err = http.NewRequest(http.MethodGet, "/live", nil)
if err != nil {
t.Fatalf(("failed to create request"))
}
response = httptest.NewRecorder()
healthz(response, request)
got = response.Code
want = 500
if got != want {
t.Fatalf("got: %q, want: %q", got, want)
}
}
// TestRunLeaderElectionWithControllers tests that leadership election works
// with real controllers and that on context cancellation the controllers stop
// running.
func TestRunLeaderElectionWithControllers(t *testing.T) {
t.Logf("Creating controller")
var controllers []*controller.Controller
for k := range kube.ResourceMap {
c, err := controller.NewController(testutil.Clients.KubernetesClient, k, testutil.Namespace, []string{}, "", "", metrics.NewCollectors())
if err != nil {
logrus.Fatalf("%s", err)
}
controllers = append(controllers, c)
}
time.Sleep(3 * time.Second)
lock := GetNewLock(testutil.Clients.KubernetesClient.CoordinationV1(), fmt.Sprintf("%s-%d", constants.LockName, 1), testutil.Pod, testutil.Namespace)
ctx, cancel := context.WithCancel(context.TODO())
// Start running leadership election, this also starts the controllers
go RunLeaderElection(lock, ctx, cancel, testutil.Pod, controllers)
time.Sleep(3 * time.Second)
// Create some stuff and do a thing
configmapName := testutil.ConfigmapNamePrefix + "-update-" + testutil.RandSeq(5)
configmapClient, err := testutil.CreateConfigMap(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName, "www.google.com")
if err != nil {
t.Fatalf("Error while creating the configmap %v", err)
}
// Creating deployment
_, err = testutil.CreateDeployment(testutil.Clients.KubernetesClient, configmapName, testutil.Namespace, true)
if err != nil {
t.Fatalf("Error in deployment creation: %v", err)
}
// Updating configmap for first time
updateErr := testutil.UpdateConfigMap(configmapClient, testutil.Namespace, configmapName, "", "www.stakater.com")
if updateErr != nil {
t.Fatalf("Configmap was not updated")
}
time.Sleep(3 * time.Second)
// Verifying deployment update
logrus.Infof("Verifying pod envvars has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com")
config := common.Config{
Namespace: testutil.Namespace,
ResourceName: configmapName,
SHAValue: shaData,
Annotation: options.ConfigmapUpdateOnChangeAnnotation,
}
deploymentFuncs := handler.GetDeploymentRollingUpgradeFuncs()
updated := testutil.VerifyResourceEnvVarUpdate(testutil.Clients, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs)
if !updated {
t.Fatalf("Deployment was not updated")
}
time.Sleep(testutil.SleepDuration)
// Cancel the leader election context, so leadership is released
logrus.Info("shutting down controller from test")
cancel()
time.Sleep(5 * time.Second)
// Updating configmap again
updateErr = testutil.UpdateConfigMap(configmapClient, testutil.Namespace, configmapName, "", "www.stakater.com/new")
if updateErr != nil {
t.Fatalf("Configmap was not updated")
}
// Verifying that the deployment was not updated as leadership has been lost
logrus.Infof("Verifying pod envvars has not been updated")
shaData = testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com/new")
config = common.Config{
Namespace: testutil.Namespace,
ResourceName: configmapName,
SHAValue: shaData,
Annotation: options.ConfigmapUpdateOnChangeAnnotation,
}
deploymentFuncs = handler.GetDeploymentRollingUpgradeFuncs()
updated = testutil.VerifyResourceEnvVarUpdate(testutil.Clients, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs)
if updated {
t.Fatalf("Deployment was updated")
}
// Deleting deployment
err = testutil.DeleteDeployment(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName)
if err != nil {
logrus.Errorf("Error while deleting the deployment %v", err)
}
// Deleting configmap
err = testutil.DeleteConfigMap(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName)
if err != nil {
logrus.Errorf("Error while deleting the configmap %v", err)
}
time.Sleep(testutil.SleepDuration)
}
+17 -18
View File
@@ -10,7 +10,7 @@ import (
"runtime"
"time"
"github.com/sirupsen/logrus"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/config"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -199,30 +199,29 @@ func (m *MetaInfo) ToConfigMap() *corev1.ConfigMap {
type Publisher struct {
client client.Client
cfg *config.Config
log logr.Logger
}
// NewPublisher creates a new Publisher.
func NewPublisher(c client.Client, cfg *config.Config) *Publisher {
func NewPublisher(c client.Client, cfg *config.Config, log logr.Logger) *Publisher {
return &Publisher{
client: c,
cfg: cfg,
log: log,
}
}
// Publish creates or updates the metadata ConfigMap.
// Returns an error if the operation fails, or nil on success.
// If RELOADER_NAMESPACE is not set, this is a no-op.
func (p *Publisher) Publish(ctx context.Context) error {
namespace := os.Getenv(EnvReloaderNamespace)
if namespace == "" {
logrus.Warn("RELOADER_NAMESPACE is not set, skipping meta info configmap creation")
p.log.Info("RELOADER_NAMESPACE is not set, skipping meta info configmap creation")
return nil
}
metaInfo := NewMetaInfo(p.cfg)
configMap := metaInfo.ToConfigMap()
// Try to get existing ConfigMap
existing := &corev1.ConfigMap{}
err := p.client.Get(ctx, client.ObjectKey{
Name: ConfigMapName,
@@ -233,34 +232,36 @@ func (p *Publisher) Publish(ctx context.Context) error {
if !errors.IsNotFound(err) {
return fmt.Errorf("failed to get existing meta info configmap: %w", err)
}
// ConfigMap doesn't exist, create it
logrus.Info("Creating meta info configmap")
p.log.Info("Creating meta info configmap")
if err := p.client.Create(ctx, configMap, client.FieldOwner(FieldManager)); err != nil {
return fmt.Errorf("failed to create meta info configmap: %w", err)
}
logrus.Info("Meta info configmap created successfully")
p.log.Info("Meta info configmap created successfully")
return nil
}
// ConfigMap exists, update it
logrus.Info("Meta info configmap already exists, updating it")
p.log.Info("Meta info configmap already exists, updating it")
existing.Data = configMap.Data
existing.Labels = configMap.Labels
if err := p.client.Update(ctx, existing, client.FieldOwner(FieldManager)); err != nil {
return fmt.Errorf("failed to update meta info configmap: %w", err)
}
logrus.Info("Meta info configmap updated successfully")
p.log.Info("Meta info configmap updated successfully")
return nil
}
// PublishMetaInfoConfigMap is a convenience function that creates a Publisher and calls Publish.
// This provides a simple API similar to the v1 PublishMetaInfoConfigmap function.
func PublishMetaInfoConfigMap(ctx context.Context, c client.Client, cfg *config.Config) error {
publisher := NewPublisher(c, cfg)
func PublishMetaInfoConfigMap(ctx context.Context, c client.Client, cfg *config.Config, log logr.Logger) error {
publisher := NewPublisher(c, cfg, log)
return publisher.Publish(ctx)
}
// toJSON marshals data to JSON string. Returns empty string on error.
// CreateOrUpdate creates or updates the metadata ConfigMap using the provided client.
func CreateOrUpdate(c client.Client, cfg *config.Config, log logr.Logger) error {
ctx := context.Background()
return PublishMetaInfoConfigMap(ctx, c, cfg, log)
}
func toJSON(data interface{}) string {
jsonData, err := json.Marshal(data)
if err != nil {
@@ -269,8 +270,6 @@ func toJSON(data interface{}) string {
return string(jsonData)
}
// parseUTCTime parses a time string in RFC3339 format.
// Returns zero time if value is empty or parsing fails.
func parseUTCTime(value string) time.Time {
if value == "" {
return time.Time{}
+10 -4
View File
@@ -6,6 +6,7 @@ import (
"os"
"testing"
"github.com/go-logr/logr"
"github.com/stakater/Reloader/internal/pkg/config"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -13,6 +14,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// testLogger returns a no-op logger for testing.
func testLogger() logr.Logger {
return logr.Discard()
}
func TestNewBuildInfo(t *testing.T) {
// Set build variables for testing
oldVersion := Version
@@ -166,7 +172,7 @@ func TestPublisher_Publish_NoNamespace(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
cfg := config.NewDefault()
publisher := NewPublisher(fakeClient, cfg)
publisher := NewPublisher(fakeClient, cfg, testLogger())
err := publisher.Publish(context.Background())
if err != nil {
@@ -188,7 +194,7 @@ func TestPublisher_Publish_CreateNew(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
cfg := config.NewDefault()
publisher := NewPublisher(fakeClient, cfg)
publisher := NewPublisher(fakeClient, cfg, testLogger())
ctx := context.Background()
err := publisher.Publish(ctx)
@@ -233,7 +239,7 @@ func TestPublisher_Publish_UpdateExisting(t *testing.T) {
Build()
cfg := config.NewDefault()
publisher := NewPublisher(fakeClient, cfg)
publisher := NewPublisher(fakeClient, cfg, testLogger())
ctx := context.Background()
err := publisher.Publish(ctx)
@@ -277,7 +283,7 @@ func TestPublishMetaInfoConfigMap(t *testing.T) {
cfg := config.NewDefault()
ctx := context.Background()
err := PublishMetaInfoConfigMap(ctx, fakeClient, cfg)
err := PublishMetaInfoConfigMap(ctx, fakeClient, cfg, testLogger())
if err != nil {
t.Errorf("PublishMetaInfoConfigMap() error = %v", err)
}
-92
View File
@@ -1,92 +0,0 @@
package options
import "github.com/stakater/Reloader/internal/pkg/constants"
type ArgoRolloutStrategy int
const (
// RestartStrategy is the annotation value for restart strategy for rollouts
RestartStrategy ArgoRolloutStrategy = iota
// RolloutStrategy is the annotation value for rollout strategy for rollouts
RolloutStrategy
)
var (
// Auto reload all resources when their corresponding configmaps/secrets are updated
AutoReloadAll = false
// ConfigmapUpdateOnChangeAnnotation is an annotation to detect changes in
// configmaps specified by name
ConfigmapUpdateOnChangeAnnotation = "configmap.reloader.stakater.com/reload"
// SecretUpdateOnChangeAnnotation is an annotation to detect changes in
// secrets specified by name
SecretUpdateOnChangeAnnotation = "secret.reloader.stakater.com/reload"
// ReloaderAutoAnnotation is an annotation to detect changes in secrets/configmaps
ReloaderAutoAnnotation = "reloader.stakater.com/auto"
// IgnoreResourceAnnotation is an annotation to ignore changes in secrets/configmaps
IgnoreResourceAnnotation = "reloader.stakater.com/ignore"
// ConfigmapReloaderAutoAnnotation is an annotation to detect changes in configmaps
ConfigmapReloaderAutoAnnotation = "configmap.reloader.stakater.com/auto"
// SecretReloaderAutoAnnotation is an annotation to detect changes in secrets
SecretReloaderAutoAnnotation = "secret.reloader.stakater.com/auto"
// ConfigmapReloaderAutoAnnotation is a comma separated list of configmaps that excludes detecting changes on cms
ConfigmapExcludeReloaderAnnotation = "configmaps.exclude.reloader.stakater.com/reload"
// SecretExcludeReloaderAnnotation is a comma separated list of secrets that excludes detecting changes on secrets
SecretExcludeReloaderAnnotation = "secrets.exclude.reloader.stakater.com/reload"
// AutoSearchAnnotation is an annotation to detect changes in
// configmaps or triggers with the SearchMatchAnnotation
AutoSearchAnnotation = "reloader.stakater.com/search"
// SearchMatchAnnotation is an annotation to tag secrets to be found with
// AutoSearchAnnotation
SearchMatchAnnotation = "reloader.stakater.com/match"
// RolloutStrategyAnnotation is an annotation to define rollout update strategy
RolloutStrategyAnnotation = "reloader.stakater.com/rollout-strategy"
// PauseDeploymentAnnotation is an annotation to define the time period to pause a deployment after
// a configmap/secret change has been detected. Valid values are described here: https://pkg.go.dev/time#ParseDuration
// only positive values are allowed
PauseDeploymentAnnotation = "deployment.reloader.stakater.com/pause-period"
// Annotation set by reloader to indicate that the deployment has been paused
PauseDeploymentTimeAnnotation = "deployment.reloader.stakater.com/paused-at"
// LogFormat is the log format to use (json, or empty string for default)
LogFormat = ""
// LogLevel is the log level to use (trace, debug, info, warning, error, fatal and panic)
LogLevel = ""
// IsArgoRollouts Adds support for argo rollouts
IsArgoRollouts = "false"
// ReloadStrategy Specify the update strategy
ReloadStrategy = constants.EnvVarsReloadStrategy
// ReloadOnCreate Adds support to watch create events
ReloadOnCreate = "false"
// ReloadOnDelete Adds support to watch delete events
ReloadOnDelete = "false"
SyncAfterRestart = false
// EnableHA adds support for running multiple replicas via leadership election
EnableHA = false
// Url to send a request to instead of triggering a reload
WebhookUrl = ""
// ResourcesToIgnore is a list of resources to ignore when watching for changes
ResourcesToIgnore = []string{}
// WorkloadTypesToIgnore is a list of workload types to ignore when watching for changes
WorkloadTypesToIgnore = []string{}
// NamespacesToIgnore is a list of namespace names to ignore when watching for changes
NamespacesToIgnore = []string{}
// NamespaceSelectors is a list of namespace selectors to watch for changes
NamespaceSelectors = []string{}
// ResourceSelectors is a list of resource selectors to watch for changes
ResourceSelectors = []string{}
// EnablePProf enables pprof for profiling
EnablePProf = false
// PProfAddr is the address to start pprof server on
// Default is :6060
PProfAddr = ":6060"
)
func ToArgoRolloutStrategy(s string) ArgoRolloutStrategy {
switch s {
case "restart":
return RestartStrategy
case "rollout":
fallthrough
default:
return RolloutStrategy
}
}
-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
}
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
package util
import (
"reflect"
"strconv"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// InterfaceSlice converts an interface to an interface array
func InterfaceSlice(slice interface{}) []interface{} {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
logrus.Errorf("InterfaceSlice() given a non-slice type")
}
ret := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
ret[i] = s.Index(i).Interface()
}
return ret
}
type ObjectMeta struct {
metav1.ObjectMeta
}
func ToObjectMeta(kubernetesObject interface{}) ObjectMeta {
objectValue := reflect.ValueOf(kubernetesObject)
fieldName := reflect.TypeOf((*metav1.ObjectMeta)(nil)).Elem().Name()
field := objectValue.FieldByName(fieldName).Interface().(metav1.ObjectMeta)
return ObjectMeta{
ObjectMeta: field,
}
}
// ParseBool returns result in bool format after parsing
func ParseBool(value interface{}) bool {
if reflect.Bool == reflect.TypeOf(value).Kind() {
return value.(bool)
} else if reflect.String == reflect.TypeOf(value).Kind() {
result, _ := strconv.ParseBool(value.(string))
return result
}
return false
}
-128
View File
@@ -1,128 +0,0 @@
package util
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/crypto"
"github.com/stakater/Reloader/internal/pkg/options"
v1 "k8s.io/api/core/v1"
)
// ConvertToEnvVarName converts the given text into a usable env var
// removing any special chars with '_' and transforming text to upper case
func ConvertToEnvVarName(text string) string {
var buffer bytes.Buffer
upper := strings.ToUpper(text)
lastCharValid := false
for i := 0; i < len(upper); i++ {
ch := upper[i]
if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
buffer.WriteString(string(ch))
lastCharValid = true
} else {
if lastCharValid {
buffer.WriteString("_")
}
lastCharValid = false
}
}
return buffer.String()
}
func GetSHAfromConfigmap(configmap *v1.ConfigMap) string {
values := []string{}
for k, v := range configmap.Data {
values = append(values, k+"="+v)
}
for k, v := range configmap.BinaryData {
values = append(values, k+"="+base64.StdEncoding.EncodeToString(v))
}
sort.Strings(values)
return crypto.GenerateSHA(strings.Join(values, ";"))
}
func GetSHAfromSecret(data map[string][]byte) string {
values := []string{}
for k, v := range data {
values = append(values, k+"="+string(v[:]))
}
sort.Strings(values)
return crypto.GenerateSHA(strings.Join(values, ";"))
}
type List []string
func (l *List) Contains(s string) bool {
for _, v := range *l {
if v == s {
return true
}
}
return false
}
func ConfigureReloaderFlags(cmd *cobra.Command) {
cmd.PersistentFlags().BoolVar(&options.AutoReloadAll, "auto-reload-all", false, "Auto reload all resources")
cmd.PersistentFlags().StringVar(&options.ConfigmapUpdateOnChangeAnnotation, "configmap-annotation", "configmap.reloader.stakater.com/reload", "annotation to detect changes in configmaps, specified by name")
cmd.PersistentFlags().StringVar(&options.SecretUpdateOnChangeAnnotation, "secret-annotation", "secret.reloader.stakater.com/reload", "annotation to detect changes in secrets, specified by name")
cmd.PersistentFlags().StringVar(&options.ReloaderAutoAnnotation, "auto-annotation", "reloader.stakater.com/auto", "annotation to detect changes in secrets/configmaps")
cmd.PersistentFlags().StringVar(&options.ConfigmapReloaderAutoAnnotation, "configmap-auto-annotation", "configmap.reloader.stakater.com/auto", "annotation to detect changes in configmaps")
cmd.PersistentFlags().StringVar(&options.SecretReloaderAutoAnnotation, "secret-auto-annotation", "secret.reloader.stakater.com/auto", "annotation to detect changes in secrets")
cmd.PersistentFlags().StringVar(&options.AutoSearchAnnotation, "auto-search-annotation", "reloader.stakater.com/search", "annotation to detect changes in configmaps or secrets tagged with special match annotation")
cmd.PersistentFlags().StringVar(&options.SearchMatchAnnotation, "search-match-annotation", "reloader.stakater.com/match", "annotation to mark secrets or configmaps to match the search")
cmd.PersistentFlags().StringVar(&options.PauseDeploymentAnnotation, "pause-deployment-annotation", "deployment.reloader.stakater.com/pause-period", "annotation to define the time period to pause a deployment after a configmap/secret change has been detected")
cmd.PersistentFlags().StringVar(&options.PauseDeploymentTimeAnnotation, "pause-deployment-time-annotation", "deployment.reloader.stakater.com/paused-at", "annotation to indicate when a deployment was paused by Reloader")
cmd.PersistentFlags().StringVar(&options.LogFormat, "log-format", "", "Log format to use (empty string for text, or JSON)")
cmd.PersistentFlags().StringVar(&options.LogLevel, "log-level", "info", "Log level to use (trace, debug, info, warning, error, fatal and panic)")
cmd.PersistentFlags().StringVar(&options.WebhookUrl, "webhook-url", "", "webhook to trigger instead of performing a reload")
cmd.PersistentFlags().StringSliceVar(&options.ResourcesToIgnore, "resources-to-ignore", options.ResourcesToIgnore, "list of resources to ignore (valid options 'configMaps' or 'secrets')")
cmd.PersistentFlags().StringSliceVar(&options.WorkloadTypesToIgnore, "ignored-workload-types", options.WorkloadTypesToIgnore, "list of workload types to ignore (valid options: 'jobs', 'cronjobs', or both)")
cmd.PersistentFlags().StringSliceVar(&options.NamespacesToIgnore, "namespaces-to-ignore", options.NamespacesToIgnore, "list of namespaces to ignore")
cmd.PersistentFlags().StringSliceVar(&options.NamespaceSelectors, "namespace-selector", options.NamespaceSelectors, "list of key:value labels to filter on for namespaces")
cmd.PersistentFlags().StringSliceVar(&options.ResourceSelectors, "resource-label-selector", options.ResourceSelectors, "list of key:value labels to filter on for configmaps and secrets")
cmd.PersistentFlags().StringVar(&options.IsArgoRollouts, "is-Argo-Rollouts", "false", "Add support for argo rollouts")
cmd.PersistentFlags().StringVar(&options.ReloadStrategy, constants.ReloadStrategyFlag, constants.EnvVarsReloadStrategy, "Specifies the desired reload strategy")
cmd.PersistentFlags().StringVar(&options.ReloadOnCreate, "reload-on-create", "false", "Add support to watch create events")
cmd.PersistentFlags().StringVar(&options.ReloadOnDelete, "reload-on-delete", "false", "Add support to watch delete events")
cmd.PersistentFlags().BoolVar(&options.EnableHA, "enable-ha", false, "Adds support for running multiple replicas via leadership election")
cmd.PersistentFlags().BoolVar(&options.SyncAfterRestart, "sync-after-restart", false, "Sync add events after reloader restarts")
cmd.PersistentFlags().BoolVar(&options.EnablePProf, "enable-pprof", false, "Enable pprof for profiling")
cmd.PersistentFlags().StringVar(&options.PProfAddr, "pprof-addr", ":6060", "Address to start pprof server on. Default is :6060")
}
func GetIgnoredResourcesList() (List, error) {
ignoredResourcesList := options.ResourcesToIgnore // getStringSliceFromFlags(cmd, "resources-to-ignore")
for _, v := range ignoredResourcesList {
if v != "configMaps" && v != "secrets" {
return nil, fmt.Errorf("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not '%s'", v)
}
}
if len(ignoredResourcesList) > 1 {
return nil, errors.New("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not both")
}
return ignoredResourcesList, nil
}
func GetIgnoredWorkloadTypesList() (List, error) {
ignoredWorkloadTypesList := options.WorkloadTypesToIgnore
for _, v := range ignoredWorkloadTypesList {
if v != "jobs" && v != "cronjobs" {
return nil, fmt.Errorf("'ignored-workload-types' accepts 'jobs', 'cronjobs', or both, not '%s'", v)
}
}
return ignoredWorkloadTypesList, nil
}
-186
View File
@@ -1,186 +0,0 @@
package util
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/options"
v1 "k8s.io/api/core/v1"
)
func TestConvertToEnvVarName(t *testing.T) {
data := "www.stakater.com"
envVar := ConvertToEnvVarName(data)
if envVar != "WWW_STAKATER_COM" {
t.Errorf("Failed to convert data into environment variable")
}
}
func TestGetHashFromConfigMap(t *testing.T) {
data := map[*v1.ConfigMap]string{
{
Data: map[string]string{"test": "test"},
}: "Only Data",
{
Data: map[string]string{"test": "test"},
BinaryData: map[string][]byte{"bintest": []byte("test")},
}: "Both Data and BinaryData",
{
BinaryData: map[string][]byte{"bintest": []byte("test")},
}: "Only BinaryData",
}
converted := map[string]string{}
for cm, cmName := range data {
converted[cmName] = GetSHAfromConfigmap(cm)
}
// Test that the has for each configmap is really unique
for cmName, cmHash := range converted {
count := 0
for _, cmHash2 := range converted {
if cmHash == cmHash2 {
count++
}
}
if count > 1 {
t.Errorf("Found duplicate hashes for %v", cmName)
}
}
}
func TestGetIgnoredWorkloadTypesList(t *testing.T) {
// Save original state
originalWorkloadTypes := options.WorkloadTypesToIgnore
defer func() {
options.WorkloadTypesToIgnore = originalWorkloadTypes
}()
tests := []struct {
name string
workloadTypes []string
expectError bool
expected []string
}{
{
name: "Both jobs and cronjobs",
workloadTypes: []string{"jobs", "cronjobs"},
expectError: false,
expected: []string{"jobs", "cronjobs"},
},
{
name: "Only jobs",
workloadTypes: []string{"jobs"},
expectError: false,
expected: []string{"jobs"},
},
{
name: "Only cronjobs",
workloadTypes: []string{"cronjobs"},
expectError: false,
expected: []string{"cronjobs"},
},
{
name: "Empty list",
workloadTypes: []string{},
expectError: false,
expected: []string{},
},
{
name: "Invalid workload type",
workloadTypes: []string{"invalid"},
expectError: true,
expected: nil,
},
{
name: "Mixed valid and invalid",
workloadTypes: []string{"jobs", "invalid"},
expectError: true,
expected: nil,
},
{
name: "Duplicate values",
workloadTypes: []string{"jobs", "jobs"},
expectError: false,
expected: []string{"jobs", "jobs"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set the global option
options.WorkloadTypesToIgnore = tt.workloadTypes
result, err := GetIgnoredWorkloadTypesList()
if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Expected no error but got: %v", err)
}
if !tt.expectError {
if len(result) != len(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
return
}
for i, expected := range tt.expected {
if i >= len(result) || result[i] != expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
break
}
}
}
})
}
}
func TestListContains(t *testing.T) {
tests := []struct {
name string
list List
item string
expected bool
}{
{
name: "List contains item",
list: List{"jobs", "cronjobs"},
item: "jobs",
expected: true,
},
{
name: "List does not contain item",
list: List{"jobs"},
item: "cronjobs",
expected: false,
},
{
name: "Empty list",
list: List{},
item: "jobs",
expected: false,
},
{
name: "Case sensitive matching",
list: List{"jobs", "cronjobs"},
item: "Jobs",
expected: false,
},
{
name: "Multiple occurrences",
list: List{"jobs", "jobs", "cronjobs"},
item: "jobs",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.list.Contains(tt.item)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}