From 1d4461898ccc6b97e7a367f97d905ada3d943e73 Mon Sep 17 00:00:00 2001 From: Steeve Chailloux Date: Wed, 18 Sep 2024 09:10:29 +0200 Subject: [PATCH] rollout update strategy annotation Signed-off-by: Steeve Chailloux --- README.md | 1 + internal/pkg/callbacks/rolling_upgrade.go | 15 ++- .../pkg/callbacks/rolling_upgrade_test.go | 105 ++++++++++++++++++ internal/pkg/options/flags.go | 22 ++++ internal/pkg/testutil/kube.go | 73 ++++++++---- 5 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 internal/pkg/callbacks/rolling_upgrade_test.go diff --git a/README.md b/README.md index ebaad021b..5beafc4fc 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ spec: - you may want to prevent watching certain resources with the `--resources-to-ignore` flag - you can configure logging in JSON format with the `--log-format=json` option - you can configure the "reload strategy" with the `--reload-strategy=` option (details below) +- you can configure rollout reload strategy with `reloader.stakater.com/rollout-strategy` annotation, `restart` or `rollout` values are available (defaults to `rollout`) ## Reload Strategies diff --git a/internal/pkg/callbacks/rolling_upgrade.go b/internal/pkg/callbacks/rolling_upgrade.go index 4ba6207e8..11da5aaea 100644 --- a/internal/pkg/callbacks/rolling_upgrade.go +++ b/internal/pkg/callbacks/rolling_upgrade.go @@ -2,10 +2,11 @@ package callbacks import ( "context" - "time" "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" @@ -329,11 +330,15 @@ func UpdateDeploymentConfig(clients kube.Clients, namespace string, resource run // UpdateRollout performs rolling upgrade on rollout func UpdateRollout(clients kube.Clients, namespace string, resource runtime.Object) error { + var err error rollout := resource.(*argorolloutv1alpha1.Rollout) - rolloutBefore, _ := clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts(namespace).Get(context.TODO(), rollout.Name, meta_v1.GetOptions{}) - logrus.Warnf("Before: %+v", rolloutBefore.Spec.Template.Spec.Containers[0].Env) - logrus.Warnf("After: %+v", rollout.Spec.Template.Spec.Containers[0].Env) - _, 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"}) + strategy := rollout.GetAnnotations()[options.RolloutStrategyAnnotation] + 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 } diff --git a/internal/pkg/callbacks/rolling_upgrade_test.go b/internal/pkg/callbacks/rolling_upgrade_test.go new file mode 100644 index 000000000..e53afaa78 --- /dev/null +++ b/internal/pkg/callbacks/rolling_upgrade_test.go @@ -0,0 +1,105 @@ +package callbacks_test + +import ( + "context" + "testing" + "time" + + argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + argorollouts "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + watch "k8s.io/apimachinery/pkg/watch" + + "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 = kube.Clients{ArgoRolloutClient: argorollouts.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("Error while creating rollout: %v", err) + } + modifiedChan := watchRollout(rollout.Name, namespace) + + err = callbacks.UpdateRollout(clients, namespace, rollout) + if err != nil { + t.Errorf("Error while updating rollout: %v", err) + } + rollout, err = clients.ArgoRolloutClient.ArgoprojV1alpha1().Rollouts( + namespace).Get(context.TODO(), rollout.Name, meta_v1.GetOptions{}) + + if err != nil { + t.Errorf("Error while 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 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(), meta_v1.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 + } + } +} diff --git a/internal/pkg/options/flags.go b/internal/pkg/options/flags.go index c252defe4..081acc3e6 100644 --- a/internal/pkg/options/flags.go +++ b/internal/pkg/options/flags.go @@ -2,6 +2,15 @@ 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 @@ -27,6 +36,8 @@ var ( // 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" // 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) @@ -45,3 +56,14 @@ var ( // Url to send a request to instead of triggering a reload WebhookUrl = "" ) + +func ToArgoRolloutStrategy(s string) ArgoRolloutStrategy { + switch s { + case "restart": + return RestartStrategy + case "rollout": + fallthrough + default: + return RolloutStrategy + } +} diff --git a/internal/pkg/testutil/kube.go b/internal/pkg/testutil/kube.go index 3faa1d22e..be488e285 100644 --- a/internal/pkg/testutil/kube.go +++ b/internal/pkg/testutil/kube.go @@ -10,6 +10,8 @@ import ( "strings" "time" + argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + argorollout "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" openshiftv1 "github.com/openshift/api/apps/v1" appsclient "github.com/openshift/client-go/apps/clientset/versioned" "github.com/sirupsen/logrus" @@ -69,16 +71,16 @@ func DeleteNamespace(namespace string, client kubernetes.Interface) { } } -func getObjectMeta(namespace string, name string, autoReload bool, secretAutoReload bool, configmapAutoReload bool) metav1.ObjectMeta { +func getObjectMeta(namespace string, name string, autoReload bool, secretAutoReload bool, configmapAutoReload bool, extraAnnotations map[string]string) metav1.ObjectMeta { return metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: map[string]string{"firstLabel": "temp"}, - Annotations: getAnnotations(name, autoReload, secretAutoReload, configmapAutoReload), + Annotations: getAnnotations(name, autoReload, secretAutoReload, configmapAutoReload, extraAnnotations), } } -func getAnnotations(name string, autoReload bool, secretAutoReload bool, configmapAutoReload bool) map[string]string { +func getAnnotations(name string, autoReload bool, secretAutoReload bool, configmapAutoReload bool, extraAnnotations map[string]string) map[string]string { annotations := make(map[string]string) if autoReload { annotations[options.ReloaderAutoAnnotation] = "true" @@ -90,13 +92,15 @@ func getAnnotations(name string, autoReload bool, secretAutoReload bool, configm annotations[options.ConfigmapReloaderAutoAnnotation] = "true" } - if len(annotations) > 0 { - return annotations - } else { - return map[string]string{ + if !(len(annotations) > 0) { + annotations = map[string]string{ options.ConfigmapUpdateOnChangeAnnotation: name, options.SecretUpdateOnChangeAnnotation: name} } + for k, v := range extraAnnotations { + annotations[k] = v + } + return annotations } func getEnvVarSources(name string) []v1.EnvFromSource { @@ -342,7 +346,7 @@ func getPodTemplateSpecWithInitContainerAndEnv(name string) v1.PodTemplateSpec { func GetDeployment(namespace string, deploymentName string) *appsv1.Deployment { replicaset := int32(1) return &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -361,7 +365,7 @@ func GetDeploymentConfig(namespace string, deploymentConfigName string) *openshi replicaset := int32(1) podTemplateSpecWithVolume := getPodTemplateSpecWithVolumes(deploymentConfigName) return &openshiftv1.DeploymentConfig{ - ObjectMeta: getObjectMeta(namespace, deploymentConfigName, false, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentConfigName, false, false, false, map[string]string{}), Spec: openshiftv1.DeploymentConfigSpec{ Replicas: replicaset, Strategy: openshiftv1.DeploymentStrategy{ @@ -376,7 +380,7 @@ func GetDeploymentConfig(namespace string, deploymentConfigName string) *openshi func GetDeploymentWithInitContainer(namespace string, deploymentName string) *appsv1.Deployment { replicaset := int32(1) return &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -394,7 +398,7 @@ func GetDeploymentWithInitContainer(namespace string, deploymentName string) *ap func GetDeploymentWithInitContainerAndEnv(namespace string, deploymentName string) *appsv1.Deployment { replicaset := int32(1) return &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -411,7 +415,7 @@ func GetDeploymentWithInitContainerAndEnv(namespace string, deploymentName strin func GetDeploymentWithEnvVars(namespace string, deploymentName string) *appsv1.Deployment { replicaset := int32(1) return &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -429,7 +433,7 @@ func GetDeploymentConfigWithEnvVars(namespace string, deploymentConfigName strin replicaset := int32(1) podTemplateSpecWithEnvVars := getPodTemplateSpecWithEnvVars(deploymentConfigName) return &openshiftv1.DeploymentConfig{ - ObjectMeta: getObjectMeta(namespace, deploymentConfigName, false, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentConfigName, false, false, false, map[string]string{}), Spec: openshiftv1.DeploymentConfigSpec{ Replicas: replicaset, Strategy: openshiftv1.DeploymentStrategy{ @@ -443,7 +447,7 @@ func GetDeploymentConfigWithEnvVars(namespace string, deploymentConfigName strin func GetDeploymentWithEnvVarSources(namespace string, deploymentName string) *appsv1.Deployment { replicaset := int32(1) return &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, true, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -460,7 +464,7 @@ func GetDeploymentWithEnvVarSources(namespace string, deploymentName string) *ap func GetDeploymentWithPodAnnotations(namespace string, deploymentName string, both bool) *appsv1.Deployment { replicaset := int32(1) deployment := &appsv1.Deployment{ - ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false), + ObjectMeta: getObjectMeta(namespace, deploymentName, false, false, false, map[string]string{}), Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -475,7 +479,7 @@ func GetDeploymentWithPodAnnotations(namespace string, deploymentName string, bo if !both { deployment.ObjectMeta.Annotations = nil } - deployment.Spec.Template.ObjectMeta.Annotations = getAnnotations(deploymentName, true, false, false) + deployment.Spec.Template.ObjectMeta.Annotations = getAnnotations(deploymentName, true, false, false, map[string]string{}) return deployment } @@ -483,9 +487,9 @@ func GetDeploymentWithTypedAutoAnnotation(namespace string, deploymentName strin replicaset := int32(1) var objectMeta metav1.ObjectMeta if resourceType == SecretResourceType { - objectMeta = getObjectMeta(namespace, deploymentName, false, true, false) + objectMeta = getObjectMeta(namespace, deploymentName, false, true, false, map[string]string{}) } else if resourceType == ConfigmapResourceType { - objectMeta = getObjectMeta(namespace, deploymentName, false, false, true) + objectMeta = getObjectMeta(namespace, deploymentName, false, false, true, map[string]string{}) } return &appsv1.Deployment{ @@ -537,7 +541,7 @@ func GetDeploymentWithExcludeAnnotation(namespace string, deploymentName string, // GetDaemonSet provides daemonset for testing func GetDaemonSet(namespace string, daemonsetName string) *appsv1.DaemonSet { return &appsv1.DaemonSet{ - ObjectMeta: getObjectMeta(namespace, daemonsetName, false, false, false), + ObjectMeta: getObjectMeta(namespace, daemonsetName, false, false, false, map[string]string{}), Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -552,7 +556,7 @@ func GetDaemonSet(namespace string, daemonsetName string) *appsv1.DaemonSet { func GetDaemonSetWithEnvVars(namespace string, daemonSetName string) *appsv1.DaemonSet { return &appsv1.DaemonSet{ - ObjectMeta: getObjectMeta(namespace, daemonSetName, true, false, false), + ObjectMeta: getObjectMeta(namespace, daemonSetName, true, false, false, map[string]string{}), Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -568,7 +572,7 @@ func GetDaemonSetWithEnvVars(namespace string, daemonSetName string) *appsv1.Dae // GetStatefulSet provides statefulset for testing func GetStatefulSet(namespace string, statefulsetName string) *appsv1.StatefulSet { return &appsv1.StatefulSet{ - ObjectMeta: getObjectMeta(namespace, statefulsetName, false, false, false), + ObjectMeta: getObjectMeta(namespace, statefulsetName, false, false, false, map[string]string{}), Spec: appsv1.StatefulSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -584,7 +588,7 @@ func GetStatefulSet(namespace string, statefulsetName string) *appsv1.StatefulSe // GetStatefulSet provides statefulset for testing func GetStatefulSetWithEnvVar(namespace string, statefulsetName string) *appsv1.StatefulSet { return &appsv1.StatefulSet{ - ObjectMeta: getObjectMeta(namespace, statefulsetName, true, false, false), + ObjectMeta: getObjectMeta(namespace, statefulsetName, true, false, false, map[string]string{}), Spec: appsv1.StatefulSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"secondLabel": "temp"}, @@ -1071,3 +1075,28 @@ func VerifyResourceAnnotationUpdate(clients kube.Clients, config util.Config, up func GetSHAfromEmptyData() string { return crypto.GenerateSHA("") } + +// GetRollout provides rollout for testing +func GetRollout(namespace string, rolloutName string, annotations map[string]string) *argorolloutv1alpha1.Rollout { + replicaset := int32(1) + return &argorolloutv1alpha1.Rollout{ + ObjectMeta: getObjectMeta(namespace, rolloutName, false, false, false, annotations), + Spec: argorolloutv1alpha1.RolloutSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"secondLabel": "temp"}, + }, + Replicas: &replicaset, + Template: getPodTemplateSpecWithVolumes(rolloutName), + }, + } +} + +// CreateRollout creates a rolout in given namespace and returns the Rollout +func CreateRollout(client argorollout.Interface, rolloutName string, namespace string, annotations map[string]string) (*argorolloutv1alpha1.Rollout, error) { + logrus.Infof("Creating Rollout") + rolloutClient := client.ArgoprojV1alpha1().Rollouts(namespace) + rolloutObj := GetRollout(namespace, rolloutName, annotations) + rollout, err := rolloutClient.Create(context.TODO(), rolloutObj, metav1.CreateOptions{}) + time.Sleep(3 * time.Second) + return rollout, err +}