From 2be5eead5712fe39c54cf72cc429619d9962119f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Fri, 10 Sep 2021 14:51:15 +0200 Subject: [PATCH] Event for Cluster Upgrade Announcement (#9) * Event for Cluster Upgrade Announcement * Event for Cluster Upgrade Announcement * Event for Cluster Upgrade Announcement * Event for Cluster Upgrade Announcement * Event for Cluster Upgrade Announcement * apply annotation * rbac * utc in timer * update cluster annotation announcement before sending event * detecting if out of office * update circleci Co-authored-by: Antonia von den Driesch --- .circleci/config.yml | 14 ++++++ CHANGELOG.md | 1 + controllers/cluster_controller.go | 45 ++++++++++++++++- controllers/utility.go | 22 +++++++-- go.mod | 4 +- go.sum | 4 +- .../templates/rbac.yaml | 8 +++- util/record/recorder.go | 48 +++++++++++++++++++ 8 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 util/record/recorder.go diff --git a/.circleci/config.yml b/.circleci/config.yml index dbce1c7..543944d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,3 +64,17 @@ workflows: filters: tags: only: /^v.*/ + + - architect/push-to-app-collection: + context: "architect" + name: push-upgrade-schedule-operator-to-aws-app-collection + app_name: "upgrade-schedule-operator" + app_collection_repo: "aws-app-collection" + requires: + - push-upgrade-schedule-operator-to-app-catalog + filters: + # Only do this when a new tag is created. + branches: + ignore: /.*/ + tags: + only: /^v.*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bff967c..4c21f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add helm manifest. +- Add Upgrade-Schedule-Operator. [Unreleased]: https://github.com/giantswarm/upgrade-schedule-operator/tree/master diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index 7a3fc2e..31c9baa 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -25,8 +25,11 @@ import ( "github.com/giantswarm/apiextensions/v3/pkg/annotation" "github.com/giantswarm/apiextensions/v3/pkg/label" "github.com/go-logr/logr" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/cluster-api/util/annotations" ctrl "sigs.k8s.io/controller-runtime" @@ -38,6 +41,8 @@ type ClusterReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + recorder record.EventRecorder } //+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete @@ -102,9 +107,34 @@ func (r *ClusterReconciler) ReconcileUpgrade(ctx context.Context, cluster *clust return ctrl.Result{}, err } + // Send scheduled cluster upgrade announcement. + if _, exists := cluster.Annotations[ClusterUpgradeAnnouncement]; !exists { + if upgradeAnnouncementTimeReached(upgradeTime) { + cluster.Annotations[ClusterUpgradeAnnouncement] = "true" + err = r.Client.Update(ctx, cluster) + if err != nil { + log.Error(err, "Failed to set upgrade announcement annotation.") + return ctrl.Result{}, err + } + log.Info("Sending cluster upgrade announcement event.") + + msg := fmt.Sprintf("The cluster %s/%s upgrade from release version %v to %v is scheduled to start in %v.", + cluster.Namespace, + cluster.Name, + getClusterReleaseVersionLabel(cluster), + getClusterUpgradeVersionAnnotation(cluster), + upgradeTime.Sub(time.Now().UTC()).Round(time.Minute), + ) + if outOfOffice(upgradeTime) { + msg += "Please contact us via urgent@giantswarm.io in case of annormalies." + } + r.sendClusterUpgradeEvent(cluster, msg) + } + } + // Return if the scheduled upgrade time is not reached yet. if !upgradeTimeReached(upgradeTime) { - log.Info(fmt.Sprintf("The scheduled update time is not reached yet. Cluster will be upgraded in %v at %v.", time.Until(upgradeTime).Round(time.Minute), upgradeTime)) + log.Info(fmt.Sprintf("The scheduled update time is not reached yet. Cluster will be upgraded in %v at %v.", upgradeTime.Sub(time.Now().UTC()).Round(time.Minute), upgradeTime)) return timedRequeue(upgradeTime), nil } @@ -130,6 +160,7 @@ func (r *ClusterReconciler) ReconcileUpgrade(ctx context.Context, cluster *clust cluster.Labels[label.ReleaseVersion] = getClusterUpgradeVersionAnnotation(cluster) delete(cluster.Annotations, annotation.UpdateScheduleTargetTime) delete(cluster.Annotations, annotation.UpdateScheduleTargetRelease) + delete(cluster.Annotations, ClusterUpgradeAnnouncement) err = r.Client.Update(ctx, cluster) if err != nil { log.Error(err, "Failed to update Release version tag and remove scheduled upgrade annotations.") @@ -142,7 +173,17 @@ func (r *ClusterReconciler) ReconcileUpgrade(ctx context.Context, cluster *clust // SetupWithManager sets up the controller with the Manager. func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). + err := ctrl.NewControllerManagedBy(mgr). For(&clusterv1.Cluster{}). Complete(r) + if err != nil { + return errors.Wrap(err, "failed setting up with a controller manager") + } + + r.recorder = mgr.GetEventRecorderFor("cluster-controller") + return nil +} + +func (r *ClusterReconciler) sendClusterUpgradeEvent(cluster *clusterv1.Cluster, message string) { + r.recorder.Eventf(cluster, corev1.EventTypeNormal, "ClusterUpgradeAnnouncement", message) } diff --git a/controllers/utility.go b/controllers/utility.go index f432211..007802a 100644 --- a/controllers/utility.go +++ b/controllers/utility.go @@ -11,6 +11,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +const ClusterUpgradeAnnouncement = "alpha.giantswarm.io/update-schedule-upgrade-announcement" + func defaultRequeue() reconcile.Result { return ctrl.Result{ Requeue: true, @@ -19,12 +21,12 @@ func defaultRequeue() reconcile.Result { } func timedRequeue(upgradeTime time.Time) reconcile.Result { - if time.Until(upgradeTime) > 5*time.Minute { + if upgradeTime.Sub(time.Now().UTC()) > 5*time.Minute { return defaultRequeue() } return ctrl.Result{ Requeue: true, - RequeueAfter: time.Until(upgradeTime) + time.Second, + RequeueAfter: upgradeTime.Sub(time.Now().UTC()) + time.Second, } } @@ -48,5 +50,19 @@ func upgradeApplied(targetVersion semver.Version, currentVersion semver.Version) } func upgradeTimeReached(upgradeTime time.Time) bool { - return upgradeTime.Before(time.Now()) + return upgradeTime.Before(time.Now().UTC()) +} + +func upgradeAnnouncementTimeReached(upgradeTime time.Time) bool { + return upgradeTime.Add(-15 * time.Minute).Before(time.Now().UTC()) +} + +func outOfOffice(upgradeTime time.Time) bool { + if upgradeTime.Day() == 6 || upgradeTime.Day() == 7 { + return true + } + if upgradeTime.UTC().Hour() <= 7 && upgradeTime.UTC().Hour() >= 16 { + return true + } + return false } diff --git a/go.mod b/go.mod index b45e7c6..e11e8a7 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.16 require ( github.com/blang/semver v3.5.1+incompatible - github.com/giantswarm/apiextensions/v3 v3.32.1-0.20210908083826-6fdda5406dde + github.com/giantswarm/apiextensions/v3 v3.33.0 github.com/go-logr/logr v0.4.0 + github.com/pkg/errors v0.9.1 + k8s.io/api v0.22.1 k8s.io/apimachinery v0.22.1 k8s.io/client-go v0.22.1 sigs.k8s.io/cluster-api v0.3.22 diff --git a/go.sum b/go.sum index 797c64f..2885399 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/giantswarm/apiextensions/v3 v3.32.1-0.20210908083826-6fdda5406dde h1:G5doh5SYz7S8xc3WNDNm0bTDIOaY9t4yLaBJWGTkVJk= -github.com/giantswarm/apiextensions/v3 v3.32.1-0.20210908083826-6fdda5406dde/go.mod h1:T0/d4PKlGUPFIMqdStzj0JVuOk51rZNQVRnZHXdYVug= +github.com/giantswarm/apiextensions/v3 v3.33.0 h1:HJb98a4pT0aA2jIPEzwhNSqTouTEm0J51Lhk4cs3ad0= +github.com/giantswarm/apiextensions/v3 v3.33.0/go.mod h1:T0/d4PKlGUPFIMqdStzj0JVuOk51rZNQVRnZHXdYVug= github.com/giantswarm/microerror v0.3.0/go.mod h1:g8oCEMFAoEs70riRRmj9+6eiz7SqNxYl+2OfxFh1po0= github.com/giantswarm/to v0.3.0/go.mod h1:RTRtw+Dyk6YqoiNBOGLO981BqhibtVwogdaFIMO1y/A= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= diff --git a/helm/upgrade-schedule-operator/templates/rbac.yaml b/helm/upgrade-schedule-operator/templates/rbac.yaml index a22deed..8a3e6ba 100644 --- a/helm/upgrade-schedule-operator/templates/rbac.yaml +++ b/helm/upgrade-schedule-operator/templates/rbac.yaml @@ -38,7 +38,13 @@ rules: resources: - events verbs: - - create + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - coordination.k8s.io resources: diff --git a/util/record/recorder.go b/util/record/recorder.go new file mode 100644 index 0000000..a33ae8b --- /dev/null +++ b/util/record/recorder.go @@ -0,0 +1,48 @@ +// Package record implements recording functionality. +package record + +import ( + "strings" + "sync" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" +) + +var ( + initOnce sync.Once + defaultRecorder record.EventRecorder +) + +func init() { + defaultRecorder = new(record.FakeRecorder) +} + +// InitFromRecorder initializes the global default recorder. It can only be called once. +// Subsequent calls are considered noops. +func InitFromRecorder(recorder record.EventRecorder) { + initOnce.Do(func() { + defaultRecorder = recorder + }) +} + +// Event constructs an event from the given information and puts it in the queue for sending. +func Event(object runtime.Object, reason, message string) { + defaultRecorder.Event(object, corev1.EventTypeNormal, strings.Title(reason), message) +} + +// Eventf is just like Event, but with Sprintf for the message field. +func Eventf(object runtime.Object, reason, message string, args ...interface{}) { + defaultRecorder.Eventf(object, corev1.EventTypeNormal, strings.Title(reason), message, args...) +} + +// Warn constructs a warning event from the given information and puts it in the queue for sending. +func Warn(object runtime.Object, reason, message string) { + defaultRecorder.Event(object, corev1.EventTypeWarning, strings.Title(reason), message) +} + +// Warnf is just like Warn, but with Sprintf for the message field. +func Warnf(object runtime.Object, reason, message string, args ...interface{}) { + defaultRecorder.Eventf(object, corev1.EventTypeWarning, strings.Title(reason), message, args...) +}