mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-27 14:37:17 +00:00
feat: Migration to new implementation
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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,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"
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user