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,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
Reference in New Issue
Block a user