Path: blob/main/components/ws-manager-mk2/controllers/maintenance_controller.go
2498 views
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package controllers56import (7"context"8"encoding/json"9"fmt"10"os"11"sync"12"time"1314"github.com/gitpod-io/gitpod/ws-manager/api/config"15"github.com/prometheus/client_golang/prometheus"16corev1 "k8s.io/api/core/v1"17"k8s.io/apimachinery/pkg/api/errors"18"k8s.io/apimachinery/pkg/types"19ctrl "sigs.k8s.io/controller-runtime"20"sigs.k8s.io/controller-runtime/pkg/client"21"sigs.k8s.io/controller-runtime/pkg/controller"22"sigs.k8s.io/controller-runtime/pkg/handler"23"sigs.k8s.io/controller-runtime/pkg/log"24"sigs.k8s.io/controller-runtime/pkg/predicate"25"sigs.k8s.io/controller-runtime/pkg/source"26)2728var (29configMapKey = types.NamespacedName{Name: "ws-manager-mk2-maintenance-mode", Namespace: "default"}30// lookupOnce is used for the first call to IsEnabled to load the maintenance mode state by looking31// up the ConfigMap, as it's possible we haven't received a reconcile event yet to load its state.32lookupOnce sync.Once33)3435func NewMaintenanceReconciler(c client.Client, reg prometheus.Registerer) (*MaintenanceReconciler, error) {36r := &MaintenanceReconciler{37Client: c,38enabledUntil: nil,39}4041gauge := newMaintenanceEnabledGauge(r)42reg.MustRegister(gauge)4344return r, nil45}4647type MaintenanceReconciler struct {48client.Client4950enabledUntil *time.Time51}5253func (r *MaintenanceReconciler) IsEnabled(ctx context.Context) bool {54// On the first call, we load the maintenance mode state from the ConfigMap,55// as it's possible we haven't reconciled it yet.56lookupOnce.Do(func() {57if err := r.loadFromCM(ctx, configMapKey); err != nil {58log.FromContext(ctx).Error(err, "cannot load maintenance mode config")59}60})6162return r.enabledUntil != nil && time.Now().Before(*r.enabledUntil)63}6465//+kubebuilder:rbac:groups=core,resources=configmap,verbs=get;list;watch6667func (r *MaintenanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {68if req.Name != configMapKey.Name || req.Namespace != configMapKey.Namespace {69// Ignore all other configmaps.70return ctrl.Result{}, nil71}7273return ctrl.Result{}, r.loadFromCM(ctx, req.NamespacedName)74}7576func (r *MaintenanceReconciler) loadFromCM(ctx context.Context, key types.NamespacedName) error {77log := log.FromContext(ctx)7879var cm corev1.ConfigMap80if err := r.Get(ctx, key, &cm); err != nil {81if errors.IsNotFound(err) {82// ConfigMap does not exist, disable maintenance mode.83r.setEnabledUntil(ctx, nil)84return nil85}8687log.Error(err, "unable to fetch configmap")88return fmt.Errorf("failed to fetch configmap: %w", err)89}9091configJson, ok := cm.Data["config.json"]92if !ok {93log.Info("missing config.json, setting maintenance mode as disabled")94r.setEnabledUntil(ctx, nil)95return nil96}9798var cfg config.MaintenanceConfig99if err := json.Unmarshal([]byte(configJson), &cfg); err != nil {100log.Error(err, "failed to unmarshal maintenance config, setting maintenance mode as disabled")101r.setEnabledUntil(ctx, nil)102return nil103}104105r.setEnabledUntil(ctx, cfg.EnabledUntil)106return nil107}108109func (r *MaintenanceReconciler) setEnabledUntil(ctx context.Context, enabledUntil *time.Time) {110if enabledUntil == r.enabledUntil {111// Nothing to do.112return113}114115r.enabledUntil = enabledUntil116log.FromContext(ctx).Info("maintenance mode state change", "enabledUntil", enabledUntil)117}118119func (r *MaintenanceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {120// We need to use an unmanaged controller to avoid issues when the pod is in standby mode.121// In that scenario, the controllers are not started and don't watch changes and only122// observe the maintenance mode during the initialization.123c, err := controller.NewUnmanaged("maintenance-controller", mgr, controller.Options{Reconciler: r})124if err != nil {125return err126}127128go func() {129err = c.Start(ctx)130if err != nil {131log.FromContext(ctx).Error(err, "cannot start maintenance reconciler")132os.Exit(1)133}134}()135136return c.Watch(source.Kind(mgr.GetCache(), &corev1.ConfigMap{}, &handler.TypedEnqueueRequestForObject[*corev1.ConfigMap]{}, predicate.NewTypedPredicateFuncs(filterMaintenanceModeConfigMap)))137}138139func filterMaintenanceModeConfigMap(obj *corev1.ConfigMap) bool {140if obj == nil {141return false142}143return obj.GetName() == configMapKey.Name && obj.GetNamespace() == configMapKey.Namespace144}145146147